diff --git a/bitnet_tools/__init__.py b/bitnet_tools/__init__.py index 316a94c..a36b517 100644 --- a/bitnet_tools/__init__.py +++ b/bitnet_tools/__init__.py @@ -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"] + diff --git a/bitnet_tools/ui/app.js b/bitnet_tools/ui/app.js new file mode 100644 index 0000000..9312dd4 --- /dev/null +++ b/bitnet_tools/ui/app.js @@ -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'); +}); diff --git a/bitnet_tools/ui/index.html b/bitnet_tools/ui/index.html new file mode 100644 index 0000000..bb7d18b --- /dev/null +++ b/bitnet_tools/ui/index.html @@ -0,0 +1,59 @@ + + + + + + BitNet CSV Analyzer + + + +
+

BitNet CSV Analyzer

+

CSV 업로드 → 자동 요약 → BitNet 답변까지 한 번에.

+ +
+ + + +
+ +
+ +
+ + + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+

데이터 요약

+

+      
+ +
+

생성 프롬프트

+ +

+      
+ +
+

BitNet 응답

+

+      
+
+ + + diff --git a/bitnet_tools/ui/styles.css b/bitnet_tools/ui/styles.css new file mode 100644 index 0000000..0ee1949 --- /dev/null +++ b/bitnet_tools/ui/styles.css @@ -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; +} diff --git a/bitnet_tools/web.py b/bitnet_tools/web.py new file mode 100644 index 0000000..97aa372 --- /dev/null +++ b/bitnet_tools/web.py @@ -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()