remove minio

This commit is contained in:
qixinbo
2026-03-15 00:10:01 +08:00
parent 02a1f11965
commit 4985c1eed3
6 changed files with 174 additions and 166 deletions
+23 -18
View File
@@ -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)
}
-51
View File
@@ -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()
-7
View File
@@ -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:
+1 -1
View File
@@ -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",
+23 -61
View File
@@ -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"
+127 -28
View File
@@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
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() {
</div>
<ScrollArea className="flex-1 h-[calc(100vh-100px)]">
{/* Hidden file input available in all states */}
<input
type="file"
ref={fileInputRef}
className="hidden"
accept=".csv,.xls,.xlsx"
onChange={handleFileUpload}
/>
<div className="min-h-full">
{messages.length <= 1 ? (
<div className="h-full flex flex-col items-center justify-center pt-[20vh] px-4 pb-32">
@@ -317,6 +375,19 @@ export function ChatInterface() {
{/* Input Area */}
<div className="w-full max-w-3xl relative">
<div className="bg-white rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.06)] border border-zinc-100 p-4 transition-shadow hover:shadow-[0_8px_40px_rgb(0,0,0,0.08)]">
{attachedFile && (
<div className="mx-2 mb-3 p-2.5 bg-blue-50/50 border border-blue-100/50 rounded-xl flex items-center justify-between">
<div className="flex items-center gap-2.5 text-sm text-blue-900">
<div className="p-1.5 bg-blue-100 rounded-md">
<FileIcon className="h-4 w-4 text-blue-600" />
</div>
<span className="font-medium truncate max-w-[300px]">{attachedFile.filename}</span>
</div>
<button onClick={() => setAttachedFile(null)} className="p-1 text-blue-400 hover:text-blue-600 hover:bg-blue-100/50 rounded-md transition-colors">
<X className="h-4 w-4" />
</button>
</div>
)}
<textarea
className="w-full min-h-[60px] max-h-[200px] resize-none border-none focus:ring-0 text-lg text-zinc-700 placeholder:text-zinc-300 bg-transparent p-2"
placeholder="先思考后回答,解决更有难度的问题"
@@ -349,8 +420,14 @@ export function ChatInterface() {
</div>
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full">
<Paperclip className="h-5 w-5" />
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
>
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
</Button>
<Button
onClick={handleSend}
@@ -422,27 +499,49 @@ export function ChatInterface() {
{messages.length > 1 && (
<div className="absolute bottom-6 left-0 right-0 px-4">
<div className="max-w-3xl mx-auto">
<div className="bg-white rounded-2xl shadow-xl border border-zinc-200/60 p-2 flex items-center gap-2 ring-1 ring-zinc-100">
<Input
className="flex-1 border-none shadow-none focus-visible:ring-0 text-base text-zinc-700 placeholder:text-zinc-400 h-11 bg-transparent"
placeholder="Send a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
disabled={isLoading}
/>
<Button
onClick={handleSend}
size="icon"
disabled={!input.trim() || isLoading}
className={`h-9 w-9 rounded-lg transition-all ${
input.trim()
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
}`}
>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
</Button>
<div className="bg-white rounded-2xl shadow-xl border border-zinc-200/60 p-2 flex flex-col gap-2 ring-1 ring-zinc-100">
{attachedFile && (
<div className="mx-2 mt-1 p-2 bg-blue-50/50 border border-blue-100/50 rounded-lg flex items-center justify-between">
<div className="flex items-center gap-2 text-xs text-blue-900">
<FileIcon className="h-3.5 w-3.5 text-blue-600" />
<span className="font-medium truncate max-w-[200px]">{attachedFile.filename}</span>
</div>
<button onClick={() => setAttachedFile(null)} className="text-blue-400 hover:text-blue-600">
<X className="h-3.5 w-3.5" />
</button>
</div>
)}
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-zinc-400 hover:text-zinc-600 rounded-full shrink-0"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading || isLoading}
>
{isUploading ? <Loader2 className="h-5 w-5 animate-spin" /> : <Paperclip className="h-5 w-5" />}
</Button>
<Input
className="flex-1 border-none shadow-none focus-visible:ring-0 text-base text-zinc-700 placeholder:text-zinc-400 h-11 bg-transparent"
placeholder="Send a message..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
disabled={isLoading}
/>
<Button
onClick={handleSend}
size="icon"
disabled={!input.trim() || isLoading}
className={`h-9 w-9 rounded-lg shrink-0 transition-all ${
input.trim()
? 'bg-blue-600 hover:bg-blue-700 text-white shadow-md'
: 'bg-zinc-100 text-zinc-300 hover:bg-zinc-100 cursor-not-allowed'
}`}
>
{isLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-5 w-5" />}
</Button>
</div>
</div>
</div>
</div>