From 4985c1eed3874f2d54f95ad423234d1e4236e8bb Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sun, 15 Mar 2026 00:10:01 +0800 Subject: [PATCH] remove minio --- backend/app/api/upload.py | 41 +++--- backend/app/connectors/minio.py | 51 ------- backend/main.py | 7 - backend/pyproject.toml | 2 +- backend/uv.lock | 84 ++++-------- frontend/src/components/ChatInterface.tsx | 155 ++++++++++++++++++---- 6 files changed, 174 insertions(+), 166 deletions(-) delete mode 100644 backend/app/connectors/minio.py diff --git a/backend/app/api/upload.py b/backend/app/api/upload.py index 786e042..0b53cb5 100644 --- a/backend/app/api/upload.py +++ b/backend/app/api/upload.py @@ -1,43 +1,48 @@ -from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks -from app.connectors.minio import minio_connector +from fastapi import APIRouter, UploadFile, File, HTTPException import pandas as pd import duckdb import io import uuid +from pathlib import Path router = APIRouter() +upload_dir = Path(__file__).resolve().parents[2] / "data" / "uploads" +upload_dir.mkdir(parents=True, exist_ok=True) -@router.post("/upload/csv") -async def upload_csv(file: UploadFile = File(...), background_tasks: BackgroundTasks = None): - if not file.filename.endswith('.csv'): - raise HTTPException(status_code=400, detail="Invalid file type. Only CSV allowed.") +@router.post("/upload/file") +async def upload_file(file: UploadFile = File(...)): + allowed_extensions = ('.csv', '.xls', '.xlsx') + if not file.filename.lower().endswith(allowed_extensions): + raise HTTPException(status_code=400, detail="Invalid file type. Only CSV and Excel files allowed.") try: content = await file.read() - file_size = len(content) + if not content: + raise HTTPException(status_code=400, detail="Empty file is not allowed.") file_obj = io.BytesIO(content) - # Generate a unique filename unique_filename = f"{uuid.uuid4()}-{file.filename}" + save_path = upload_dir / unique_filename + save_path.write_bytes(content) + file_url = f"local://{unique_filename}" - # Upload to MinIO - minio_url = minio_connector.upload_file(unique_filename, file_obj, file_size, content_type="text/csv") - - # Reset file pointer for analysis file_obj.seek(0) - # Load into DuckDB (in-memory) for quick analysis try: - df = pd.read_csv(file_obj) + if file.filename.lower().endswith('.csv'): + df = pd.read_csv(file_obj) + else: + df = pd.read_excel(file_obj) + duckdb_conn = duckdb.connect(database=':memory:') - duckdb_conn.register('uploaded_csv', df) - summary = duckdb_conn.execute("DESCRIBE uploaded_csv").fetchall() + duckdb_conn.register('uploaded_file', df) + summary = duckdb_conn.execute("DESCRIBE uploaded_file").fetchall() row_count = len(df) columns = list(df.columns) return { "filename": unique_filename, - "url": minio_url, + "url": file_url, "rows": row_count, "columns": columns, "summary": str(summary) @@ -45,7 +50,7 @@ async def upload_csv(file: UploadFile = File(...), background_tasks: BackgroundT except Exception as e: return { "filename": unique_filename, - "url": minio_url, + "url": file_url, "analysis_error": str(e) } diff --git a/backend/app/connectors/minio.py b/backend/app/connectors/minio.py deleted file mode 100644 index 8c7cc29..0000000 --- a/backend/app/connectors/minio.py +++ /dev/null @@ -1,51 +0,0 @@ -from minio import Minio -from minio.error import S3Error -import os -from typing import BinaryIO - -class MinioConnector: - def __init__(self): - self.endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000") - self.access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin") - self.secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin") - self.secure = os.getenv("MINIO_SECURE", "False").lower() == "true" - self.bucket_name = os.getenv("MINIO_BUCKET", "dataclaw") - - self.client = Minio( - self.endpoint, - access_key=self.access_key, - secret_key=self.secret_key, - secure=self.secure - ) - self._ensure_bucket_exists() - - def _ensure_bucket_exists(self): - try: - if not self.client.bucket_exists(self.bucket_name): - self.client.make_bucket(self.bucket_name) - except S3Error as e: - print(f"MinIO Bucket Error: {e}") - - def upload_file(self, object_name: str, file_data: BinaryIO, length: int, content_type: str = "application/octet-stream"): - try: - self.client.put_object( - self.bucket_name, - object_name, - file_data, - length, - content_type=content_type - ) - return f"http{'s' if self.secure else ''}://{self.endpoint}/{self.bucket_name}/{object_name}" - except S3Error as e: - print(f"MinIO Upload Error: {e}") - raise e - - def test_connection(self) -> bool: - try: - self.client.list_buckets() - return True - except Exception as e: - print(f"MinIO Connection Error: {e}") - return False - -minio_connector = MinioConnector() diff --git a/backend/main.py b/backend/main.py index f1d1815..843005a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,7 +9,6 @@ import json from app.api import upload, llm, skills, users from app.connectors.postgres import postgres_connector from app.connectors.clickhouse import clickhouse_connector -from app.connectors.minio import minio_connector from app.core.nanobot import nanobot_service from app.core.session_alias_store import session_alias_store from app.agent.nl2sql import process_nl2sql, NL2SQLRequest, NL2SQLResponse @@ -57,12 +56,6 @@ def test_clickhouse(): return {"status": "success", "message": "Connected to ClickHouse"} raise HTTPException(status_code=500, detail="Failed to connect to ClickHouse") -@app.get("/connect/minio") -def test_minio(): - if minio_connector.test_connection(): - return {"status": "success", "message": "Connected to MinIO"} - raise HTTPException(status_code=500, detail="Failed to connect to MinIO") - @app.get("/nanobot/status") def nanobot_status(): if nanobot_service.agent: diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d468603..c3f9010 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -18,11 +18,11 @@ dependencies = [ "litellm>=1.81.5,<2.0.0", "loguru>=0.7.3,<1.0.0", "mcp>=1.26.0,<2.0.0", - "minio>=7.2.20", "msgpack>=1.1.0,<2.0.0", "nanobot-ai", "oauth-cli-kit>=0.1.3,<1.0.0", "openai>=2.8.0", + "openpyxl>=3.1.5", "pandas>=3.0.1", "passlib>=1.7.4", "prompt-toolkit>=3.0.50,<4.0.0", diff --git a/backend/uv.lock b/backend/uv.lock index 63c33d3..e22a7d7 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -177,49 +177,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" }, ] -[[package]] -name = "argon2-cffi" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi-bindings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, - { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, - { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, - { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, - { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, - { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, - { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, -] - [[package]] name = "attrs" version = "25.4.0" @@ -247,11 +204,11 @@ dependencies = [ { name = "litellm" }, { name = "loguru" }, { name = "mcp" }, - { name = "minio" }, { name = "msgpack" }, { name = "nanobot-ai" }, { name = "oauth-cli-kit" }, { name = "openai" }, + { name = "openpyxl" }, { name = "pandas" }, { name = "passlib" }, { name = "prompt-toolkit" }, @@ -291,11 +248,11 @@ requires-dist = [ { name = "litellm", specifier = ">=1.81.5,<2.0.0" }, { name = "loguru", specifier = ">=0.7.3,<1.0.0" }, { name = "mcp", specifier = ">=1.26.0,<2.0.0" }, - { name = "minio", specifier = ">=7.2.20" }, { name = "msgpack", specifier = ">=1.1.0,<2.0.0" }, { name = "nanobot-ai", directory = "../nanobot" }, { name = "oauth-cli-kit", specifier = ">=0.1.3,<1.0.0" }, { name = "openai", specifier = ">=2.8.0" }, + { name = "openpyxl", specifier = ">=3.1.5" }, { name = "pandas", specifier = ">=3.0.1" }, { name = "passlib", specifier = ">=1.7.4" }, { name = "prompt-toolkit", specifier = ">=3.0.50,<4.0.0" }, @@ -796,6 +753,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + [[package]] name = "fastapi" version = "0.135.1" @@ -1586,22 +1552,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "minio" -version = "7.2.20" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi" }, - { name = "certifi" }, - { name = "pycryptodome" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, -] - [[package]] name = "msgpack" version = "1.1.2" @@ -1958,6 +1908,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/df122348638885526e53140e9c6b0d844af7312682b3bde9587eebc28b47/openai-2.28.0-py3-none-any.whl", hash = "sha256:79aa5c45dba7fef84085701c235cf13ba88485e1ef4f8dfcedc44fc2a698fc1d", size = 1141218, upload-time = "2026-03-13T19:56:25.46Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "packaging" version = "26.0" diff --git a/frontend/src/components/ChatInterface.tsx b/frontend/src/components/ChatInterface.tsx index e8e1633..7adb011 100644 --- a/frontend/src/components/ChatInterface.tsx +++ b/frontend/src/components/ChatInterface.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check } from "lucide-react"; +import { User, Loader2, Sparkles, Search, ArrowUp, ChevronDown, Table, Paperclip, Check, X, File as FileIcon } from "lucide-react"; import { api } from "@/lib/api"; import { useVisualizationStore } from "@/store/visualizationStore"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; @@ -56,6 +56,11 @@ export function ChatInterface() { const queryParams = new URLSearchParams(location.search); const activeSessionKey = queryParams.get("session") || "api:default"; + // File upload state + const [attachedFile, setAttachedFile] = useState<{ filename: string; url: string; columns?: string[]; summary?: string } | null>(null); + const [isUploading, setIsUploading] = useState(false); + const fileInputRef = useRef(null); + useEffect(() => { fetchModels(); }, []); @@ -110,6 +115,45 @@ export function ChatInterface() { { icon: Search, label: "深度问数", color: "text-blue-500", bg: "bg-blue-50" }, ]; + const handleFileUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setIsUploading(true); + const formData = new FormData(); + formData.append("file", file); + + try { + const response = await fetch("/api/v1/upload/file", { + method: "POST", + body: formData, + headers: { + ...(localStorage.getItem("token") ? { Authorization: `Bearer ${localStorage.getItem("token")}` } : {}), + } + }); + + if (!response.ok) { + throw new Error("Upload failed"); + } + + const data = await response.json(); + setAttachedFile({ + filename: file.name, + url: data.url, + columns: data.columns, + summary: data.summary, + }); + } catch (error) { + console.error("File upload error:", error); + // Could show a toast notification here + } finally { + setIsUploading(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + }; + useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollIntoView({ behavior: 'smooth' }); @@ -122,6 +166,13 @@ export function ChatInterface() { const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input }; setMessages(prev => [...prev, newMessage]); setInput(""); + + let messagePayload = newMessage.content; + if (attachedFile) { + messagePayload = `[用户上传了文件: ${attachedFile.filename}]\n[文件内容摘要: ${attachedFile.summary || "无"}]\n[数据列: ${attachedFile.columns?.join(", ") || "无"}]\n[文件下载链接: ${attachedFile.url}]\n\n${newMessage.content}`; + setAttachedFile(null); + } + setIsLoading(true); setVizLoading(true); setVizError(null); @@ -145,7 +196,7 @@ export function ChatInterface() { ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: JSON.stringify({ - message: newMessage.content, + message: messagePayload, session_id: activeSessionKey, model_id: effectiveModelId, }), @@ -203,7 +254,7 @@ export function ChatInterface() { if (!streamedText) { const fallback = await api.post<{ response: string }>("/nanobot/chat", { - message: newMessage.content, + message: messagePayload, session_id: activeSessionKey, model_id: effectiveModelId, }); @@ -217,7 +268,7 @@ export function ChatInterface() { // Fallback to existing NL2SQL or other skills (e.g. for "表格问答" or "深度问数") const source = selectedDataSource.split('-')[0]; // postgres-main -> postgres const response = await api.post<{sql?: string, result?: unknown, error?: string}>('/api/v1/agent/nl2sql', { - query: newMessage.content, + query: messagePayload, source: source, session_id: activeSessionKey, model_id: selectedModelId @@ -300,7 +351,14 @@ export function ChatInterface() { - + {/* Hidden file input available in all states */} +
{messages.length <= 1 ? (
@@ -317,6 +375,19 @@ export function ChatInterface() { {/* Input Area */}
+ {attachedFile && ( +
+
+
+ +
+ {attachedFile.filename} +
+ +
+ )}