Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bitnet_tools/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
"""Utilities for BitNet-focused local data analysis workflows."""


from .analysis import (
build_analysis_payload,
build_analysis_payload_from_csv_text,
build_prompt,
summarize_rows,
)

__all__ = [
"build_analysis_payload",
"build_analysis_payload_from_csv_text",
"build_prompt",
"summarize_rows",
]

from .analysis import build_analysis_payload, summarize_rows

__all__ = ["build_analysis_payload", "summarize_rows"]

70 changes: 70 additions & 0 deletions bitnet_tools/ui/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const csvFile = document.getElementById('csvFile');
const csvText = document.getElementById('csvText');
const question = document.getElementById('question');
const model = document.getElementById('model');
const analyzeBtn = document.getElementById('analyzeBtn');
const runBtn = document.getElementById('runBtn');
const summary = document.getElementById('summary');
const prompt = document.getElementById('prompt');
const answer = document.getElementById('answer');

let latestPrompt = '';

csvFile.addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
csvText.value = await file.text();
});

document.querySelectorAll('.chip').forEach((chip) => {
chip.addEventListener('click', () => {
question.value = chip.dataset.q;
});
});

document.getElementById('copyPrompt').addEventListener('click', async () => {
if (!latestPrompt) return;
await navigator.clipboard.writeText(latestPrompt);
});

analyzeBtn.addEventListener('click', async () => {
summary.textContent = '분석 중...';
const res = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csv_text: csvText.value,
question: question.value,
}),
});
const data = await res.json();
if (!res.ok) {
summary.textContent = data.error || 'error';
return;
}

latestPrompt = data.prompt;
summary.textContent = JSON.stringify(data.summary, null, 2);
prompt.textContent = data.prompt;
answer.textContent = '';
});

runBtn.addEventListener('click', async () => {
if (!latestPrompt) {
answer.textContent = '먼저 분석을 실행해 프롬프트를 생성하세요.';
return;
}
if (!model.value.trim()) {
answer.textContent = '모델 태그를 입력하세요. 예: bitnet:latest';
return;
}

answer.textContent = 'BitNet 실행 중...';
const res = await fetch('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: model.value.trim(), prompt: latestPrompt }),
});
const data = await res.json();
answer.textContent = res.ok ? data.answer : (data.error || 'error');
});
59 changes: 59 additions & 0 deletions bitnet_tools/ui/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BitNet CSV Analyzer</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<main class="container">
<h1>BitNet CSV Analyzer</h1>
<p class="sub">CSV 업로드 → 자동 요약 → BitNet 답변까지 한 번에.</p>

<section class="panel">
<label>CSV 파일</label>
<input id="csvFile" type="file" accept=".csv,text/csv" />
<textarea id="csvText" rows="8" placeholder="또는 CSV 내용을 직접 붙여넣기"></textarea>
</section>

<section class="panel">
<label>질문</label>
<div class="chips">
<button class="chip" data-q="핵심 인사이트 3개와 근거를 알려줘">인사이트</button>
<button class="chip" data-q="이상치 의심 포인트와 추가 확인 항목을 알려줘">이상치</button>
<button class="chip" data-q="실행 가능한 다음 액션 5개를 우선순위로 제안해줘">다음행동</button>
</div>
<textarea id="question" rows="3">핵심 인사이트 3개와 근거를 알려줘</textarea>
</section>

<section class="panel row">
<div>
<label>BitNet 모델 태그 (선택)</label>
<input id="model" placeholder="bitnet:latest" />
</div>
<div class="actions">
<button id="analyzeBtn">1) 분석</button>
<button id="runBtn">2) BitNet 실행</button>
</div>
</section>

<section class="panel">
<h3>데이터 요약</h3>
<pre id="summary"></pre>
</section>

<section class="panel">
<h3>생성 프롬프트</h3>
<button id="copyPrompt">복사</button>
<pre id="prompt"></pre>
</section>

<section class="panel">
<h3>BitNet 응답</h3>
<pre id="answer"></pre>
</section>
</main>
<script src="/app.js"></script>
</body>
</html>
58 changes: 58 additions & 0 deletions bitnet_tools/ui/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
:root {
color-scheme: dark;
--bg: #111827;
--panel: #1f2937;
--text: #f9fafb;
--muted: #9ca3af;
--accent: #22c55e;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
}
.container {
max-width: 980px;
margin: 24px auto;
padding: 0 16px 40px;
}
.sub { color: var(--muted); }
.panel {
background: var(--panel);
border-radius: 10px;
padding: 14px;
margin: 12px 0;
}
.row { display: flex; justify-content: space-between; gap: 12px; align-items: end; }
label { display: block; margin-bottom: 8px; color: var(--muted); }
textarea, input {
width: 100%;
background: #0b1220;
color: var(--text);
border: 1px solid #334155;
border-radius: 8px;
padding: 10px;
}
button {
border: none;
border-radius: 8px;
padding: 10px 14px;
background: var(--accent);
color: #052e16;
font-weight: 700;
cursor: pointer;
}
.chips { display: flex; gap: 8px; margin-bottom: 8px; }
.chip { background: #334155; color: #e2e8f0; }
.actions { display: flex; gap: 8px; }
pre {
white-space: pre-wrap;
background: #0b1220;
border: 1px solid #334155;
border-radius: 8px;
padding: 10px;
max-height: 320px;
overflow: auto;
}
95 changes: 95 additions & 0 deletions bitnet_tools/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from __future__ import annotations

from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import subprocess
from urllib.parse import urlparse

from .analysis import build_analysis_payload_from_csv_text


UI_DIR = Path(__file__).parent / "ui"


def run_ollama(model: str, prompt: str) -> str:
proc = subprocess.run(
["ollama", "run", model, prompt],
capture_output=True,
text=True,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or "ollama run failed")
return proc.stdout.strip()


class Handler(BaseHTTPRequestHandler):
def _send_json(self, data: dict, status: int = HTTPStatus.OK) -> None:
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)

def _send_file(self, path: Path, content_type: str) -> None:
if not path.exists():
self.send_error(HTTPStatus.NOT_FOUND)
return
data = path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)

def do_GET(self) -> None:
route = urlparse(self.path).path
if route == "/" or route == "/index.html":
return self._send_file(UI_DIR / "index.html", "text/html; charset=utf-8")
if route == "/app.js":
return self._send_file(UI_DIR / "app.js", "application/javascript; charset=utf-8")
if route == "/styles.css":
return self._send_file(UI_DIR / "styles.css", "text/css; charset=utf-8")
self.send_error(HTTPStatus.NOT_FOUND)

def do_POST(self) -> None:
route = urlparse(self.path).path
content_length = int(self.headers.get("Content-Length", "0"))
raw = self.rfile.read(content_length)
try:
payload = json.loads(raw.decode("utf-8")) if raw else {}
except json.JSONDecodeError:
return self._send_json({"error": "invalid json"}, HTTPStatus.BAD_REQUEST)

try:
if route == "/api/analyze":
csv_text = str(payload.get("csv_text", ""))
question = str(payload.get("question", "")).strip()
if not csv_text.strip():
return self._send_json({"error": "csv_text is required"}, HTTPStatus.BAD_REQUEST)
if not question:
question = "이 데이터의 핵심 인사이트를 알려줘"
result = build_analysis_payload_from_csv_text(csv_text, question)
return self._send_json(result)

if route == "/api/run":
model = str(payload.get("model", "")).strip()
prompt = str(payload.get("prompt", "")).strip()
if not model or not prompt:
return self._send_json({"error": "model and prompt are required"}, HTTPStatus.BAD_REQUEST)
answer = run_ollama(model, prompt)
return self._send_json({"answer": answer})

except Exception as exc: # runtime surface for UI
return self._send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)

self.send_error(HTTPStatus.NOT_FOUND)


def serve(host: str = "127.0.0.1", port: int = 8765) -> None:
server = ThreadingHTTPServer((host, port), Handler)
print(f"BitNet UI running at http://{host}:{port}")
server.serve_forever()
Loading