restart
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +25 -0
- README.md +13 -0
- api_service.py +145 -0
- app.py +6 -0
- build/asset-manifest.json +15 -0
- build/favicon.ico +0 -0
- build/index.html +1 -0
- build/logo192.png +3 -0
- build/logo512.png +3 -0
- build/manifest.json +25 -0
- build/robots.txt +3 -0
- build/static/css/main.1eeb7222.css +2 -0
- build/static/css/main.1eeb7222.css.map +1 -0
- build/static/js/453.ed3810f9.chunk.js +2 -0
- build/static/js/453.ed3810f9.chunk.js.map +1 -0
- build/static/js/main.d1af9f99.js +0 -0
- build/static/js/main.d1af9f99.js.LICENSE.txt +71 -0
- build/static/js/main.d1af9f99.js.map +0 -0
- database/processed_documents.db +3 -0
- dockerfile +34 -0
- main.py +143 -0
- package-lock.json +0 -0
- package.json +46 -0
- public/favicon.ico +0 -0
- public/index.html +43 -0
- public/logo192.png +3 -0
- public/logo512.png +3 -0
- public/manifest.json +25 -0
- public/robots.txt +3 -0
- requirements.txt +17 -0
- setup_knowledge_base.py +376 -0
- src/App.css +67 -0
- src/App.tsx +19 -0
- src/ChatPage.css +94 -0
- src/ChatPage.tsx +90 -0
- src/LandingPage.tsx +123 -0
- src/foot.css +166 -0
- src/index.css +13 -0
- src/index.tsx +20 -0
- src/logo.svg +1 -0
- src/picture/linkedin-icon.png +3 -0
- src/picture/logo.png +3 -0
- src/react-app-env.d.ts +1 -0
- src/reportWebVitals.ts +15 -0
- src/setupTests.ts +5 -0
- tsconfig.json +26 -0
- vector_db_chroma/chroma.sqlite3 +3 -0
- vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/data_level0.bin +3 -0
- vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/header.bin +3 -0
- vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/length.bin +3 -0
.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
2 |
+
|
3 |
+
# dependencies
|
4 |
+
/node_modules
|
5 |
+
/.pnp
|
6 |
+
.pnp.js
|
7 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# production
|
12 |
+
.venv/
|
13 |
+
__pycache__/
|
14 |
+
input/
|
15 |
+
.env
|
16 |
+
# misc
|
17 |
+
.DS_Store
|
18 |
+
.env.local
|
19 |
+
.env.development.local
|
20 |
+
.env.test.local
|
21 |
+
.env.production.local
|
22 |
+
|
23 |
+
npm-debug.log*
|
24 |
+
yarn-debug.log*
|
25 |
+
yarn-error.log*
|
README.md
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: LabAid V2
|
3 |
+
emoji: 🧪
|
4 |
+
colorFrom: indigo
|
5 |
+
colorTo: red
|
6 |
+
sdk: docker
|
7 |
+
app_file: Dockerfile
|
8 |
+
pinned: false
|
9 |
+
---
|
10 |
+
|
11 |
+
# LabAid V2 - HPLC Instrument Assistant
|
12 |
+
|
13 |
+
This Space combines a FastAPI RAG backend (powered by LangChain + Together AI) and a React frontend. It provides HPLC troubleshooting assistance by retrieving knowledge from a Chroma vector database.
|
api_service.py
ADDED
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sqlite3 # May be used elsewhere
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from fastapi import FastAPI, HTTPException
|
5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
6 |
+
from pydantic import BaseModel
|
7 |
+
from typing import List
|
8 |
+
from contextlib import asynccontextmanager
|
9 |
+
|
10 |
+
from langchain.schema import Document
|
11 |
+
from langchain_together import ChatTogether, TogetherEmbeddings
|
12 |
+
from langchain_core.prompts import ChatPromptTemplate
|
13 |
+
from langchain_core.output_parsers import StrOutputParser
|
14 |
+
from langchain_core.runnables import RunnablePassthrough
|
15 |
+
from langchain_core.documents import Document
|
16 |
+
|
17 |
+
import chromadb
|
18 |
+
from langchain_community.vectorstores import Chroma # ✅ Updated import
|
19 |
+
|
20 |
+
# --- 1. Environment & Constants ---
|
21 |
+
load_dotenv()
|
22 |
+
|
23 |
+
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
24 |
+
if not TOGETHER_API_KEY:
|
25 |
+
raise ValueError("TOGETHER_API_KEY environment variable not set. Please check your .env file.")
|
26 |
+
|
27 |
+
VECTOR_DB_DIR = "vector_db_chroma"
|
28 |
+
COLLECTION_NAME = "my_instrument_manual_chunks"
|
29 |
+
|
30 |
+
LLM_MODEL_NAME = "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
|
31 |
+
EMBEDDINGS_MODEL_NAME = "togethercomputer/m2-bert-80M-32k-retrieval"
|
32 |
+
|
33 |
+
# --- 2. Lifespan Event Handler ---
|
34 |
+
@asynccontextmanager
|
35 |
+
async def lifespan(app: FastAPI):
|
36 |
+
global rag_chain, retriever, prompt, llm
|
37 |
+
|
38 |
+
print("--- Initializing RAG components ---")
|
39 |
+
try:
|
40 |
+
llm = ChatTogether(
|
41 |
+
model=LLM_MODEL_NAME,
|
42 |
+
temperature=0.3,
|
43 |
+
api_key=TOGETHER_API_KEY
|
44 |
+
)
|
45 |
+
print(f"LLM {LLM_MODEL_NAME} initialized.")
|
46 |
+
|
47 |
+
embeddings = TogetherEmbeddings(
|
48 |
+
model=EMBEDDINGS_MODEL_NAME,
|
49 |
+
api_key=TOGETHER_API_KEY
|
50 |
+
)
|
51 |
+
client = chromadb.PersistentClient(path=VECTOR_DB_DIR)
|
52 |
+
vectorstore = Chroma(
|
53 |
+
client=client,
|
54 |
+
collection_name=COLLECTION_NAME,
|
55 |
+
embedding_function=embeddings
|
56 |
+
)
|
57 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
|
58 |
+
print("Retriever initialized.")
|
59 |
+
|
60 |
+
answer_prompt = """ You are a professional HPLC instrument troubleshooting expert who specializes in helping junior researchers and students.
|
61 |
+
Your task is to answer the user's troubleshooting questions in detail and clearly based on the HPLC instrument knowledge provided below.
|
62 |
+
If there is no direct answer in the knowledge, please provide the most reasonable speculative suggestions based on your expert judgment, or ask further clarifying questions.
|
63 |
+
Please ensure that your answers are logically clear, easy to understand, and directly address the user's questions."""
|
64 |
+
prompt = ChatPromptTemplate.from_messages([
|
65 |
+
("system", answer_prompt),
|
66 |
+
("user", "context: {context}\n\nquestion: {question}"),
|
67 |
+
])
|
68 |
+
|
69 |
+
def format_docs(docs: List[Document]) -> str:
|
70 |
+
return "\n\n".join(doc.page_content for doc in docs)
|
71 |
+
|
72 |
+
rag_chain = (
|
73 |
+
{"context": retriever | format_docs, "question": RunnablePassthrough()}
|
74 |
+
| prompt
|
75 |
+
| llm
|
76 |
+
| StrOutputParser()
|
77 |
+
)
|
78 |
+
print("RAG chain ready.")
|
79 |
+
|
80 |
+
except Exception as e:
|
81 |
+
raise RuntimeError(f"Failed to initialize RAG chain: {e}")
|
82 |
+
|
83 |
+
yield # Keep app running
|
84 |
+
|
85 |
+
# --- 3. Initialize FastAPI App ---
|
86 |
+
app = FastAPI(
|
87 |
+
title="LabAid AI",
|
88 |
+
description="API service for a Retrieval-Augmented Generation (RAG) AI assistant.",
|
89 |
+
version="1.0.0",
|
90 |
+
lifespan=lifespan # ✅ Updated lifespan hook
|
91 |
+
)
|
92 |
+
|
93 |
+
# --- 4. CORS Middleware ---
|
94 |
+
origins = [
|
95 |
+
"http://localhost",
|
96 |
+
"http://localhost:3000",
|
97 |
+
"http://127.0.0.1:8000",
|
98 |
+
]
|
99 |
+
|
100 |
+
app.add_middleware(
|
101 |
+
CORSMiddleware,
|
102 |
+
allow_origins=origins,
|
103 |
+
allow_credentials=True,
|
104 |
+
allow_methods=["*"],
|
105 |
+
allow_headers=["*"],
|
106 |
+
)
|
107 |
+
|
108 |
+
# --- 5. Request/Response Models ---
|
109 |
+
class QueryRequest(BaseModel):
|
110 |
+
query: str
|
111 |
+
|
112 |
+
class QueryResponse(BaseModel):
|
113 |
+
answer: str
|
114 |
+
source_documents: List[str]
|
115 |
+
|
116 |
+
# --- 6. RAG Query Endpoint ---
|
117 |
+
@app.post("/ask", response_model=QueryResponse)
|
118 |
+
async def ask_rag(request: QueryRequest):
|
119 |
+
if rag_chain is None or retriever is None:
|
120 |
+
raise HTTPException(status_code=500, detail="RAG chain not initialized.")
|
121 |
+
|
122 |
+
try:
|
123 |
+
user_query = request.query
|
124 |
+
print(f"Received query: {user_query}")
|
125 |
+
|
126 |
+
retrieved_docs = retriever.invoke(user_query)
|
127 |
+
formatted_context = "\n\n".join(doc.page_content for doc in retrieved_docs)
|
128 |
+
|
129 |
+
answer = (prompt | llm | StrOutputParser()).invoke({
|
130 |
+
"context": formatted_context,
|
131 |
+
"question": user_query
|
132 |
+
})
|
133 |
+
|
134 |
+
sources = [doc.page_content for doc in retrieved_docs]
|
135 |
+
return QueryResponse(answer=answer, source_documents=sources)
|
136 |
+
|
137 |
+
except Exception as e:
|
138 |
+
print(f"Error: {e}")
|
139 |
+
raise HTTPException(status_code=500, detail=f"Failed to process query: {e}")
|
140 |
+
|
141 |
+
# --- 7. Run Command Hint ---
|
142 |
+
# Run this API with:
|
143 |
+
# uvicorn api_service:app --reload --port 8000
|
144 |
+
# The --port 8000 parameter specifies the port where the service will run.
|
145 |
+
# Open your browser at http://127.0.0.1:8000/docs#/default/ask_rag_ask_post
|
app.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app.py
|
2 |
+
import uvicorn
|
3 |
+
from api_service import app # 假設你目前這份程式碼存成 api_service.py
|
4 |
+
|
5 |
+
if __name__ == "__main__":
|
6 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
build/asset-manifest.json
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"files": {
|
3 |
+
"main.css": "./static/css/main.1eeb7222.css",
|
4 |
+
"main.js": "./static/js/main.d1af9f99.js",
|
5 |
+
"static/js/453.ed3810f9.chunk.js": "./static/js/453.ed3810f9.chunk.js",
|
6 |
+
"index.html": "./index.html",
|
7 |
+
"main.1eeb7222.css.map": "./static/css/main.1eeb7222.css.map",
|
8 |
+
"main.d1af9f99.js.map": "./static/js/main.d1af9f99.js.map",
|
9 |
+
"453.ed3810f9.chunk.js.map": "./static/js/453.ed3810f9.chunk.js.map"
|
10 |
+
},
|
11 |
+
"entrypoints": [
|
12 |
+
"static/css/main.1eeb7222.css",
|
13 |
+
"static/js/main.d1af9f99.js"
|
14 |
+
]
|
15 |
+
}
|
build/favicon.ico
ADDED
|
build/index.html
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="./favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="./logo192.png"/><link rel="manifest" href="./manifest.json"/><title>React App</title><script defer="defer" src="./static/js/main.d1af9f99.js"></script><link href="./static/css/main.1eeb7222.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
build/logo192.png
ADDED
![]() |
Git LFS Details
|
build/logo512.png
ADDED
![]() |
Git LFS Details
|
build/manifest.json
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"short_name": "React App",
|
3 |
+
"name": "Create React App Sample",
|
4 |
+
"icons": [
|
5 |
+
{
|
6 |
+
"src": "favicon.ico",
|
7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
8 |
+
"type": "image/x-icon"
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"src": "logo192.png",
|
12 |
+
"type": "image/png",
|
13 |
+
"sizes": "192x192"
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"src": "logo512.png",
|
17 |
+
"type": "image/png",
|
18 |
+
"sizes": "512x512"
|
19 |
+
}
|
20 |
+
],
|
21 |
+
"start_url": ".",
|
22 |
+
"display": "standalone",
|
23 |
+
"theme_color": "#000000",
|
24 |
+
"background_color": "#ffffff"
|
25 |
+
}
|
build/robots.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# https://www.robotstxt.org/robotstxt.html
|
2 |
+
User-agent: *
|
3 |
+
Disallow:
|
build/static/css/main.1eeb7222.css
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.chat-container{font-family:Arial,sans-serif;height:100vh;margin:auto;max-width:800px}.chat-header{background-color:#4a90e2;font-size:1.5rem;padding:16px}.chat-messages{background-color:#f5f5f5;padding:16px}.chat-bubble{word-wrap:break-word;border-radius:16px;max-width:70%;padding:12px;white-space:pre-wrap}.chat-bubble.user{align-self:flex-end;background-color:#dcf8c6}.chat-bubble.ai{align-self:flex-start;background-color:#fff;border:1px solid #ddd}.chat-bubble.loading{font-style:italic;opacity:.6}.chat-input-area{background-color:#fff;border-top:1px solid #ccc;padding:12px}.chat-input-area input{border:1px solid #ccc;margin-right:8px;padding:8px}.chat-input-area button{background-color:#4a90e2;padding:8px 16px}.chat-input-area button:disabled{background-color:#a0c4f2;cursor:not-allowed}.back-button{background-color:#f2f2f2;border:none;border-radius:8px;cursor:pointer;float:right;font-size:14px;margin-right:20px;padding:6px 12px}.back-button:hover{background-color:#ddd}.App{text-align:center}.App-logo{height:40vmin;pointer-events:none}@media (prefers-reduced-motion:no-preference){.App-logo{animation:App-logo-spin 20s linear infinite}}.App-header{align-items:center;background-color:#282c34;color:#fff;display:flex;flex-direction:column;font-size:calc(10px + 2vmin);justify-content:center;min-height:100vh}.App-link{color:#61dafb}@keyframes App-logo-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.landing-container{display:flex;flex-direction:column;font-family:Arial,sans-serif;min-height:100vh}.landing-footer,.landing-header{align-items:center;background:#f8f9fb;display:flex;justify-content:space-between;padding:20px 40px}.landing-main{align-items:center;display:flex;flex:1 1;flex-direction:column;padding:40px}.landing-header .logo{font-size:1.5rem;font-weight:700}.landing-nav a,.lang-select{color:#222;margin:0 10px;text-decoration:none}.start-btn{background:#2563eb;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:1.1rem;margin:24px 0;padding:14px 32px}.how-it-works{margin:40px 0;max-width:900px;width:100%}.how-it-works h2{text-align:center}.steps{display:flex;justify-content:space-between}.step{background:#fff;border-radius:12px;box-shadow:0 2px 8px #e5e7eb;flex:1 1;margin:0 10px;padding:24px;text-align:center}.step-number{align-items:center;background:#2563eb;border-radius:50%;color:#fff;display:flex;font-size:1.2rem;height:40px;justify-content:center;margin:0 auto 12px;width:40px}.download-resources{margin:40px 0;max-width:900px;width:100%}.download-resources h2{text-align:center}.resources{display:flex;justify-content:space-between}.resource-card{background:#fff;border-radius:12px;box-shadow:0 2px 8px #e5e7eb;flex:1 1;margin:0 10px;padding:24px;text-align:center}.view-all{color:#2563eb;display:block;margin-top:16px;text-align:right;text-decoration:none}.chat-container{background:#f4f6fa;display:flex;flex-direction:column;min-height:100vh}.chat-header{background:#2563eb;color:#fff;font-size:1.3rem;padding:20px;text-align:center}.chat-messages{display:flex;flex:1 1;flex-direction:column;gap:12px;overflow-y:auto;padding:24px}.chat-message{border-radius:16px;margin-bottom:8px;max-width:60%;padding:12px 18px}.chat-message.user{align-self:flex-end;background:#2563eb;color:#fff}.chat-message.ai{align-self:flex-start;background:#fff;border:1px solid #e5e7eb;color:#222}.chat-input-area{background:#fff;border-top:1px solid #e5e7eb;display:flex;padding:16px}.chat-input-area input{border:1px solid #e5e7eb;border-radius:8px;flex:1 1;font-size:1rem;margin-right:10px;padding:10px}.chat-input-area button{background:#2563eb;border:none;border-radius:8px;color:#fff;cursor:pointer;font-size:1rem;padding:10px 24px}.labassist-footer{align-items:center;background-color:#112;display:flex;flex-direction:column;padding:60px 40px}.footer-main-content{display:flex;flex-wrap:wrap;justify-content:space-between;margin-bottom:50px;max-width:1200px;width:100%}.footer-column{flex-basis:22%;margin-bottom:20px}.footer-column h4{color:#fff;font-size:1.1em;font-weight:700;margin-bottom:15px}.footer-column ul{list-style:none}.footer-column ul li{margin-bottom:8px}.footer-column ul li a{color:#b0b0b0;font-size:.95em;text-decoration:none;transition:color .3s ease}.footer-column ul li a:hover{color:#fff}.company-info{flex-basis:30%;min-width:250px}.company-logo-section{align-items:center;display:flex;margin-bottom:15px}.footer-logo{height:30px;margin-right:10px}.company-logo-section span{color:#fff;font-size:1.2em;font-weight:700}.company-info p{color:#b0b0b0;font-size:.9em;line-height:1.6;margin-bottom:20px}.social-icons{display:flex;gap:15px}.social-icons a img{filter:invert(100%) brightness(80%);height:20px;transition:filter .3s ease;width:20px}.social-icons a:hover img{filter:invert(100%) brightness(100%)}.footer-bottom-bar{align-items:center;border-top:1px solid #334;color:#889;display:flex;font-size:.85em;justify-content:space-between;max-width:1200px;padding-top:30px;width:100%}.footer-legal-links a{color:#889;margin-left:20px;text-decoration:none;transition:color .3s ease}.footer-legal-links a:hover{color:#fff}@media (max-width:768px){.labassist-footer{padding:40px 20px}.footer-main-content{align-items:flex-start;flex-direction:column;gap:30px}.footer-column{flex-basis:100%;max-width:100%}.footer-bottom-bar{flex-direction:column;text-align:center}.footer-legal-links{margin-top:15px}.footer-legal-links a{margin:0 10px}}@media (max-width:480px){.labassist-footer{padding:30px 15px}.footer-main-content{gap:20px}.social-icons{gap:10px}}
|
2 |
+
/*# sourceMappingURL=main.1eeb7222.css.map*/
|
build/static/css/main.1eeb7222.css.map
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"version":3,"file":"static/css/main.1eeb7222.css","mappings":"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","sources":["index.css","ChatPage.css","App.css","foot.css"],"sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n",".chat-container {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100vh;\r\n max-width: 800px;\r\n margin: auto;\r\n font-family: Arial, sans-serif;\r\n}\r\n\r\n.chat-header {\r\n padding: 16px;\r\n background-color: #4a90e2;\r\n color: white;\r\n font-size: 1.5rem;\r\n text-align: center;\r\n}\r\n\r\n.chat-messages {\r\n flex: 1;\r\n padding: 16px;\r\n overflow-y: auto;\r\n background-color: #f5f5f5;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 12px;\r\n}\r\n\r\n.chat-bubble {\r\n max-width: 70%;\r\n padding: 12px;\r\n border-radius: 16px;\r\n white-space: pre-wrap;\r\n word-wrap: break-word;\r\n}\r\n\r\n.chat-bubble.user {\r\n align-self: flex-end;\r\n background-color: #dcf8c6;\r\n}\r\n\r\n.chat-bubble.ai {\r\n align-self: flex-start;\r\n background-color: #ffffff;\r\n border: 1px solid #ddd;\r\n}\r\n\r\n.chat-bubble.loading {\r\n font-style: italic;\r\n opacity: 0.6;\r\n}\r\n\r\n.chat-input-area {\r\n display: flex;\r\n padding: 12px;\r\n border-top: 1px solid #ccc;\r\n background-color: #fff;\r\n}\r\n\r\n.chat-input-area input {\r\n flex: 1;\r\n padding: 8px;\r\n border: 1px solid #ccc;\r\n border-radius: 8px;\r\n margin-right: 8px;\r\n}\r\n\r\n.chat-input-area button {\r\n padding: 8px 16px;\r\n background-color: #4a90e2;\r\n color: white;\r\n border: none;\r\n border-radius: 8px;\r\n cursor: pointer;\r\n}\r\n\r\n.chat-input-area button:disabled {\r\n background-color: #a0c4f2;\r\n cursor: not-allowed;\r\n}\r\n\r\n.back-button {\r\n float: right;\r\n margin-right: 20px;\r\n background-color: #f2f2f2;\r\n border: none;\r\n padding: 6px 12px;\r\n border-radius: 8px;\r\n cursor: pointer;\r\n font-size: 14px;\r\n}\r\n\r\n.back-button:hover {\r\n background-color: #ddd;\r\n}",".App {\n text-align: center;\n}\n\n.App-logo {\n height: 40vmin;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .App-logo {\n animation: App-logo-spin infinite 20s linear;\n }\n}\n\n.App-header {\n background-color: #282c34;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Landing Page Styles */\n.landing-container { font-family: Arial, sans-serif; min-height: 100vh; display: flex; flex-direction: column; }\n.landing-header, .landing-footer { background: #f8f9fb; padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; }\n.landing-main { flex: 1; padding: 40px; display: flex; flex-direction: column; align-items: center; }\n.landing-header .logo { font-weight: bold; font-size: 1.5rem; }\n.landing-nav a, .lang-select { margin: 0 10px; text-decoration: none; color: #222; }\n.start-btn { background: #2563eb; color: #fff; border: none; padding: 14px 32px; border-radius: 8px; font-size: 1.1rem; margin: 24px 0; cursor: pointer; }\n.how-it-works { margin: 40px 0; width: 100%; max-width: 900px; }\n.how-it-works h2 {text-align: center; }\n.steps { display: flex; justify-content: space-between; }\n.step { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #e5e7eb; padding: 24px; flex: 1; margin: 0 10px; text-align: center; }\n.step-number { background: #2563eb; color: #fff; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 1.2rem; }\n.download-resources { margin: 40px 0; width: 100%; max-width: 900px; }\n.download-resources h2 {text-align: center; }\n.resources { display: flex; justify-content: space-between; }\n.resource-card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #e5e7eb; padding: 24px; flex: 1; margin: 0 10px; text-align: center; }\n.view-all { display: block; margin-top: 16px; text-align: right; color: #2563eb; text-decoration: none; }\n\n/* Chat Page Styles */\n.chat-container { min-height: 100vh; display: flex; flex-direction: column; background: #f4f6fa; }\n.chat-header { background: #2563eb; color: #fff; padding: 20px; font-size: 1.3rem; text-align: center; }\n.chat-messages { flex: 1; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }\n.chat-message { max-width: 60%; padding: 12px 18px; border-radius: 16px; margin-bottom: 8px; }\n.chat-message.user { align-self: flex-end; background: #2563eb; color: #fff; }\n.chat-message.ai { align-self: flex-start; background: #fff; color: #222; border: 1px solid #e5e7eb; }\n.chat-input-area { display: flex; padding: 16px; background: #fff; border-top: 1px solid #e5e7eb; }\n.chat-input-area input { flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #e5e7eb; margin-right: 10px; font-size: 1rem; }\n.chat-input-area button { background: #2563eb; color: #fff; border: none; border-radius: 8px; padding: 10px 24px; font-size: 1rem; cursor: pointer; }\n",".labassist-footer {\r\n background-color: #111122; /* 頁腳背景色,比 body 略深 */\r\n padding: 60px 40px; /* 上下左右內邊距 */\r\n display: flex;\r\n flex-direction: column; /* 讓主要內容和底部版權資訊垂直堆疊 */\r\n align-items: center; /* 讓整個 footer 的內容水平居中(在有 max-width 的情況下) */\r\n}\r\n\r\n/* 主要內容區域:包含四個列 */\r\n.footer-main-content {\r\n display: flex;\r\n justify-content: space-between; /* 將四個列均勻分佈在水平空間 */\r\n width: 100%; /* 佔滿父容器寬度 */\r\n max-width: 1200px; /* 限制內容的最大寬度,使其不會太寬 */\r\n margin-bottom: 50px; /* 與底部版權資訊的間距 */\r\n flex-wrap: wrap; /* 允許在小螢幕上換行 */\r\n}\r\n\r\n/* 每一個列(公司資訊、Product、Support、Company) */\r\n.footer-column {\r\n flex-basis: 22%; /* 讓每列佔據大約 22% 的空間,以便容納 4 列並留有間距 */\r\n /* 可以根據內容調整 flex-basis,或者使用 flex-grow: 1; 讓它們自動分配空間 */\r\n margin-bottom: 20px; /* 小螢幕換行時的間距 */\r\n}\r\n\r\n.footer-column h4 {\r\n font-size: 1.1em;\r\n color: #FFFFFF; /* 標題文字白色 */\r\n margin-bottom: 15px; /* 標題與下方連結的間距 */\r\n font-weight: bold;\r\n}\r\n\r\n.footer-column ul {\r\n list-style: none; /* 移除列表點 */\r\n}\r\n\r\n.footer-column ul li {\r\n margin-bottom: 8px; /* 連結之間的間距 */\r\n}\r\n\r\n.footer-column ul li a {\r\n color: #B0B0B0; /* 連結文字顏色 */\r\n text-decoration: none; /* 移除下劃線 */\r\n font-size: 0.95em;\r\n transition: color 0.3s ease; /* 平滑過渡效果 */\r\n}\r\n\r\n.footer-column ul li a:hover {\r\n color: #FFFFFF; /* 鼠標懸停時變白 */\r\n}\r\n\r\n/* 左側公司資訊區塊特有樣式 */\r\n.company-info {\r\n flex-basis: 30%; /* 給公司資訊列更多空間 */\r\n min-width: 250px; /* 確保在小螢幕上有足夠寬度 */\r\n}\r\n\r\n.company-logo-section {\r\n display: flex;\r\n align-items: center;\r\n margin-bottom: 15px;\r\n}\r\n\r\n.footer-logo {\r\n height: 30px; /* Logo 高度 */\r\n margin-right: 10px;\r\n}\r\n\r\n.company-logo-section span {\r\n font-size: 1.2em;\r\n font-weight: bold;\r\n color: #FFFFFF;\r\n}\r\n\r\n.company-info p {\r\n font-size: 0.9em;\r\n line-height: 1.6;\r\n margin-bottom: 20px;\r\n color: #B0B0B0;\r\n}\r\n\r\n.social-icons {\r\n display: flex;\r\n gap: 15px; /* 圖標間距 */\r\n}\r\n\r\n.social-icons a img {\r\n width: 20px; /* 圖標大小 */\r\n height: 20px;\r\n filter: invert(100%) brightness(80%); /* 將圖標顏色反轉為淺色 */\r\n transition: filter 0.3s ease;\r\n}\r\n\r\n.social-icons a:hover img {\r\n filter: invert(100%) brightness(100%); /* 鼠標懸停時變亮 */\r\n}\r\n\r\n\r\n/* 底部版權資訊區域 */\r\n.footer-bottom-bar {\r\n border-top: 1px solid #333344; /* 上方加一條細分隔線 */\r\n padding-top: 30px;\r\n width: 100%;\r\n max-width: 1200px; /* 與主要內容寬度保持一致 */\r\n display: flex;\r\n justify-content: space-between; /* 版權資訊靠左,法律連結靠右 */\r\n align-items: center; /* 垂直居中對齊 */\r\n font-size: 0.85em;\r\n color: #888899; /* 較淺的灰色 */\r\n}\r\n\r\n.footer-legal-links a {\r\n color: #888899;\r\n text-decoration: none;\r\n margin-left: 20px; /* 連結之間的間距 */\r\n transition: color 0.3s ease;\r\n}\r\n\r\n.footer-legal-links a:hover {\r\n color: #FFFFFF;\r\n}\r\n\r\n/* 響應式設計:小螢幕適應 */\r\n@media (max-width: 768px) {\r\n .labassist-footer {\r\n padding: 40px 20px;\r\n }\r\n\r\n .footer-main-content {\r\n flex-direction: column; /* 小螢幕時,列變成垂直堆疊 */\r\n align-items: flex-start; /* 讓每個列都靠左對齊 */\r\n gap: 30px; /* 列之間的間距 */\r\n }\r\n\r\n .footer-column {\r\n flex-basis: 100%; /* 每列佔據全部寬度 */\r\n max-width: 100%; /* 確保最大寬度 */\r\n }\r\n\r\n .footer-bottom-bar {\r\n flex-direction: column; /* 版權和法律連結垂直堆疊 */\r\n text-align: center;\r\n }\r\n\r\n .footer-legal-links {\r\n margin-top: 15px; /* 與版權資訊的間距 */\r\n }\r\n\r\n .footer-legal-links a {\r\n margin: 0 10px; /* 調整連結間距 */\r\n }\r\n}\r\n\r\n@media (max-width: 480px) {\r\n .labassist-footer {\r\n padding: 30px 15px;\r\n }\r\n\r\n .footer-main-content {\r\n gap: 20px;\r\n }\r\n\r\n .social-icons {\r\n gap: 10px;\r\n }\r\n}"],"names":[],"sourceRoot":""}
|
build/static/js/453.ed3810f9.chunk.js
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
"use strict";(self.webpackChunkmy_app=self.webpackChunkmy_app||[]).push([[453],{453:(e,t,n)=>{n.r(t),n.d(t,{getCLS:()=>y,getFCP:()=>g,getFID:()=>C,getLCP:()=>P,getTTFB:()=>D});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,p=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=p(),d(),s((function(){setTimeout((function(){v=p(),d()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime<i.firstHiddenTime&&(r.value=e.startTime,r.entries.push(e),n(!0)))},o=window.performance&&performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint")[0],f=o?null:c("paint",a);(o||f)&&(n=m(e,r,t),o&&a(o),s((function(i){r=u("FCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,n(!0)}))}))})))},h=!1,T=-1,y=function(e,t){h||(g((function(e){T=e.value})),h=!0);var n,i=function(t){T>-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},p=c("layout-shift",v);p&&(n=m(i,r,t),f((function(){p.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r<a-w){var e={entryType:"first-input",name:i.type,target:i.target,cancelable:i.cancelable,startTime:i.timeStamp,processingStart:i.timeStamp+r};o.forEach((function(t){t(e)})),o=[]}},b=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),p=function(e){e.startTime<a.firstHiddenTime&&(v.value=e.processingStart-e.startTime,v.entries.push(e),n(!0))},d=c("first-input",p);n=m(e,v,t),d&&f((function(){d.takeRecords().map(p),d.disconnect()}),!0),d&&s((function(){var a;v=u("FID"),n=m(e,v,t),o=[],r=-1,i=null,F(addEventListener),a=p,o.push(a),S()}))},k={},P=function(e,t){var n,i=l(),r=u("LCP"),a=function(e){var t=e.startTime;t<i.firstHiddenTime&&(r.value=t,r.entries.push(e),n())},o=c("largest-contentful-paint",a);if(o){n=m(e,r,t);var v=function(){k[r.id]||(o.takeRecords().map(a),o.disconnect(),k[r.id]=!0,n(!0))};["keydown","click"].forEach((function(e){addEventListener(e,v,{once:!0,capture:!0})})),f(v,!0),s((function(i){r=u("LCP"),n=m(e,r,t),requestAnimationFrame((function(){requestAnimationFrame((function(){r.value=performance.now()-i.timeStamp,k[r.id]=!0,n(!0)}))}))}))}},D=function(e){var t,n=u("TTFB");t=function(){try{var t=performance.getEntriesByType("navigation")[0]||function(){var e=performance.timing,t={entryType:"navigation",startTime:0};for(var n in e)"navigationStart"!==n&&"toJSON"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]);
|
2 |
+
//# sourceMappingURL=453.ed3810f9.chunk.js.map
|
build/static/js/453.ed3810f9.chunk.js.map
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"version":3,"file":"static/js/453.ed3810f9.chunk.js","mappings":"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","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime<i.firstHiddenTime&&(o.value=e.startTime,o.entries.push(e),n(!0)))},s=window.performance&&performance.getEntriesByName&&performance.getEntriesByName(\"first-contentful-paint\")[0],m=s?null:a(\"paint\",f);(s||m)&&(n=c(e,o,t),s&&f(s),u((function(i){o=r(\"FCP\"),n=c(e,o,t),requestAnimationFrame((function(){requestAnimationFrame((function(){o.value=performance.now()-i.timeStamp,n(!0)}))}))})))},p=!1,l=-1,h=function(e,t){p||(d((function(e){l=e.value})),p=!0);var n,i=function(t){l>-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t<n-y){var r={entryType:\"first-input\",name:e.type,target:e.target,cancelable:e.cancelable,startTime:e.timeStamp,processingStart:e.timeStamp+t};i.forEach((function(e){e(r)})),i=[]}},S=function(e){if(e.cancelable){var t=(e.timeStamp>1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTime<m.firstHiddenTime&&(d.value=e.processingStart-e.startTime,d.entries.push(e),s(!0))},l=a(\"first-input\",p);s=c(n,d,f),l&&o((function(){l.takeRecords().map(p),l.disconnect()}),!0),l&&u((function(){var a;d=r(\"FID\"),s=c(n,d,f),i=[],t=-1,e=null,w(addEventListener),a=p,i.push(a),E()}))},b={},F=function(e,t){var n,i=v(),f=r(\"LCP\"),s=function(e){var t=e.startTime;t<i.firstHiddenTime&&(f.value=t,f.entries.push(e),n())},m=a(\"largest-contentful-paint\",s);if(m){n=c(e,f,t);var d=function(){b[f.id]||(m.takeRecords().map(s),m.disconnect(),b[f.id]=!0,n(!0))};[\"keydown\",\"click\"].forEach((function(e){addEventListener(e,d,{once:!0,capture:!0})})),o(d,!0),u((function(i){f=r(\"LCP\"),n=c(e,f,t),requestAnimationFrame((function(){requestAnimationFrame((function(){f.value=performance.now()-i.timeStamp,b[f.id]=!0,n(!0)}))}))}))}},P=function(e){var t,n=r(\"TTFB\");t=function(){try{var t=performance.getEntriesByType(\"navigation\")[0]||function(){var e=performance.timing,t={entryType:\"navigation\",startTime:0};for(var n in e)\"navigationStart\"!==n&&\"toJSON\"!==n&&(t[n]=Math.max(e[n]-e.navigationStart,0));return t}();if(n.value=n.delta=t.responseStart,n.value<0||n.value>performance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""}
|
build/static/js/main.d1af9f99.js
ADDED
The diff for this file is too large to render.
See raw diff
|
|
build/static/js/main.d1af9f99.js.LICENSE.txt
ADDED
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* @license React
|
3 |
+
* react-dom-client.production.js
|
4 |
+
*
|
5 |
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
6 |
+
*
|
7 |
+
* This source code is licensed under the MIT license found in the
|
8 |
+
* LICENSE file in the root directory of this source tree.
|
9 |
+
*/
|
10 |
+
|
11 |
+
/**
|
12 |
+
* @license React
|
13 |
+
* react-dom.production.js
|
14 |
+
*
|
15 |
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
16 |
+
*
|
17 |
+
* This source code is licensed under the MIT license found in the
|
18 |
+
* LICENSE file in the root directory of this source tree.
|
19 |
+
*/
|
20 |
+
|
21 |
+
/**
|
22 |
+
* @license React
|
23 |
+
* react-jsx-runtime.production.js
|
24 |
+
*
|
25 |
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
26 |
+
*
|
27 |
+
* This source code is licensed under the MIT license found in the
|
28 |
+
* LICENSE file in the root directory of this source tree.
|
29 |
+
*/
|
30 |
+
|
31 |
+
/**
|
32 |
+
* @license React
|
33 |
+
* react.production.js
|
34 |
+
*
|
35 |
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
36 |
+
*
|
37 |
+
* This source code is licensed under the MIT license found in the
|
38 |
+
* LICENSE file in the root directory of this source tree.
|
39 |
+
*/
|
40 |
+
|
41 |
+
/**
|
42 |
+
* @license React
|
43 |
+
* scheduler.production.js
|
44 |
+
*
|
45 |
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
46 |
+
*
|
47 |
+
* This source code is licensed under the MIT license found in the
|
48 |
+
* LICENSE file in the root directory of this source tree.
|
49 |
+
*/
|
50 |
+
|
51 |
+
/**
|
52 |
+
* @remix-run/router v1.23.0
|
53 |
+
*
|
54 |
+
* Copyright (c) Remix Software Inc.
|
55 |
+
*
|
56 |
+
* This source code is licensed under the MIT license found in the
|
57 |
+
* LICENSE.md file in the root directory of this source tree.
|
58 |
+
*
|
59 |
+
* @license MIT
|
60 |
+
*/
|
61 |
+
|
62 |
+
/**
|
63 |
+
* React Router v6.30.1
|
64 |
+
*
|
65 |
+
* Copyright (c) Remix Software Inc.
|
66 |
+
*
|
67 |
+
* This source code is licensed under the MIT license found in the
|
68 |
+
* LICENSE.md file in the root directory of this source tree.
|
69 |
+
*
|
70 |
+
* @license MIT
|
71 |
+
*/
|
build/static/js/main.d1af9f99.js.map
ADDED
The diff for this file is too large to render.
See raw diff
|
|
database/processed_documents.db
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:3bdefd49f0d6b66e66b97cf2f699ae75fa5aa6963380031c31d7a61e4b3d6ce0
|
3 |
+
size 999424
|
dockerfile
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use a base image with Python
|
2 |
+
FROM python:3.10-slim
|
3 |
+
|
4 |
+
# Set environment variables
|
5 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
6 |
+
ENV PYTHONUNBUFFERED=1
|
7 |
+
|
8 |
+
# Set working directory
|
9 |
+
WORKDIR /app
|
10 |
+
|
11 |
+
# Install system dependencies
|
12 |
+
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
13 |
+
|
14 |
+
# Install Node.js (for serving frontend if needed)
|
15 |
+
RUN apt-get install -y curl && \
|
16 |
+
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
17 |
+
apt-get install -y nodejs
|
18 |
+
|
19 |
+
# Copy and install Python dependencies
|
20 |
+
COPY requirements.txt ./
|
21 |
+
RUN pip install --upgrade pip && pip install -r requirements.txt
|
22 |
+
|
23 |
+
# Copy all files
|
24 |
+
COPY . .
|
25 |
+
|
26 |
+
# Set environment variable for Together API key (set via HF secrets)
|
27 |
+
ENV TOGETHER_API_KEY=${TOGETHER_API_KEY}
|
28 |
+
|
29 |
+
# Build React frontend
|
30 |
+
RUN cd ./ && npm install && npm run build
|
31 |
+
|
32 |
+
# Use uvicorn to launch FastAPI
|
33 |
+
EXPOSE 7860
|
34 |
+
CMD ["uvicorn", "api_service:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#main.py 負責構建 RAG 鏈。
|
2 |
+
|
3 |
+
import os
|
4 |
+
import chromadb
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
from langchain_together import TogetherEmbeddings, ChatTogether
|
8 |
+
from langchain.vectorstores import Chroma
|
9 |
+
from langchain_core.prompts import ChatPromptTemplate
|
10 |
+
from langchain_core.output_parsers import StrOutputParser
|
11 |
+
from langchain_core.runnables import RunnablePassthrough
|
12 |
+
from langchain.chains import create_retrieval_chain
|
13 |
+
from langchain.chains.combine_documents import create_stuff_documents_chain
|
14 |
+
from langchain_core.documents import Document
|
15 |
+
|
16 |
+
# Load environment variables from .env file
|
17 |
+
load_dotenv()
|
18 |
+
|
19 |
+
# --- Configuration ---
|
20 |
+
db_directory = "database"
|
21 |
+
db_path = os.path.join(db_directory, "processed_documents.db")
|
22 |
+
vector_db_dir = "vector_db_chroma"
|
23 |
+
collection_name = "my_instrument_manual_chunks"
|
24 |
+
|
25 |
+
# Ensure TOGETHER_API_KEY is set
|
26 |
+
together_api_key = os.getenv("TOGETHER_API_KEY")
|
27 |
+
if not together_api_key:
|
28 |
+
# 更好的錯誤處理:直接拋出異常,應用啟動就會失敗,避免後續問題
|
29 |
+
raise ValueError("TOGETHER_API_KEY environment variable not set. Please set it in your .env file.")
|
30 |
+
|
31 |
+
# --- LLM Setup ---
|
32 |
+
try:
|
33 |
+
llm = ChatTogether(
|
34 |
+
model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", # 或您選擇的其他 Together AI 模型
|
35 |
+
temperature=0.3,
|
36 |
+
api_key=together_api_key
|
37 |
+
)
|
38 |
+
print("LLM (Together AI) Instantiation succeeded.")
|
39 |
+
except Exception as e:
|
40 |
+
llm = None
|
41 |
+
print(f"Error instantiating LLM: {e}")
|
42 |
+
print("Please check your TOGETHER_API_KEY and model name.")
|
43 |
+
|
44 |
+
# --- Retriever Setup ---
|
45 |
+
embeddings_model_name = "togethercomputer/m2-bert-80M-32k-retrieval" # 必須與 setup_knowledge_base.py 中使用的模型一致
|
46 |
+
|
47 |
+
try:
|
48 |
+
retriever_embeddings = TogetherEmbeddings(
|
49 |
+
model=embeddings_model_name,
|
50 |
+
api_key=together_api_key
|
51 |
+
)
|
52 |
+
|
53 |
+
# Instantiate ChromaDB client and load the collection
|
54 |
+
client = chromadb.PersistentClient(path=vector_db_dir)
|
55 |
+
vectorstore = Chroma(
|
56 |
+
client=client,
|
57 |
+
collection_name=collection_name,
|
58 |
+
embedding_function=retriever_embeddings # 用於將查詢嵌入
|
59 |
+
)
|
60 |
+
# Check if the collection is empty, if so, warn the user to run setup_knowledge_base.py
|
61 |
+
if vectorstore._collection.count() == 0:
|
62 |
+
print(f"Warning: ChromaDB collection '{collection_name}' is empty. Please run 'python setup_knowledge_base.py' to populate the knowledge base.")
|
63 |
+
|
64 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
|
65 |
+
print(f"Retriever (ChromaDB) Instantiation succeeded, retrieving {retriever.search_kwargs['k']} chunks.")
|
66 |
+
|
67 |
+
except Exception as e:
|
68 |
+
retriever = None
|
69 |
+
print(f"Error setting up ChromaDB retriever: {e}")
|
70 |
+
print("Please ensure you have run 'python setup_knowledge_base.py' to create and populate the vector database.")
|
71 |
+
|
72 |
+
# --- Prompt Template Setup ---
|
73 |
+
answer_prompt = """
|
74 |
+
You are a professional HPLC instrument troubleshooting expert who specializes in helping junior researchers and students.
|
75 |
+
Your task is to answer the user's troubleshooting questions in detail and clearly based on the HPLC instrument knowledge provided below.
|
76 |
+
If there is no direct answer in the knowledge, please provide the most reasonable speculative suggestions based on your expert judgment, or ask further clarifying questions.
|
77 |
+
Please ensure that your answers are logically clear, easy to understand, and directly address the user's questions.
|
78 |
+
"""
|
79 |
+
|
80 |
+
# --- RAG Chain Construction ---
|
81 |
+
rag_chain = None # 預設為 None
|
82 |
+
if llm and retriever: # Only build the chain if both LLM and retriever were successfully initialized
|
83 |
+
try:
|
84 |
+
# format_docs 函數用於將檢索到的 LangChain Document 對象轉換為字符串
|
85 |
+
def format_docs(docs):
|
86 |
+
return "\n\n".join(doc.page_content for doc in docs if hasattr(doc, 'page_content'))
|
87 |
+
|
88 |
+
# 新的 RAG 鏈結構:
|
89 |
+
# 1. 接收一個問題 (str)
|
90 |
+
# 2. 將問題傳遞給檢索器 (retriever),獲取相關文檔
|
91 |
+
# 3. 將文檔格式化 (format_docs)
|
92 |
+
# 4. 將格式化後的文檔作為 context,原始問題作為 question,填充到 ChatPromptTemplate
|
93 |
+
# 5. 將填充後的 prompt 傳遞給 LLM
|
94 |
+
# 6. 使用 StrOutputParser() 將 LLM 輸出解析為字符串
|
95 |
+
|
96 |
+
# 修改 Prompt Template
|
97 |
+
prompt = ChatPromptTemplate.from_messages([
|
98 |
+
("system", answer_prompt),
|
99 |
+
("user", "Context: {context}\n\nQuestion: {question}"),
|
100 |
+
])
|
101 |
+
print("Prompt Template build success.")
|
102 |
+
|
103 |
+
rag_chain = (
|
104 |
+
{
|
105 |
+
"context": retriever | format_docs,
|
106 |
+
"question": RunnablePassthrough() # 這裡將接收到的輸入直接作為 'question' 傳遞
|
107 |
+
}
|
108 |
+
| prompt
|
109 |
+
| llm
|
110 |
+
| StrOutputParser()
|
111 |
+
)
|
112 |
+
print("RAG LangChain assemble success.")
|
113 |
+
except Exception as e:
|
114 |
+
rag_chain = None
|
115 |
+
print(f"Error assembling RAG LangChain: {e}")
|
116 |
+
print("Please check previous setup steps for LLM and Retriever.")
|
117 |
+
else:
|
118 |
+
print("RAG LangChain could not be assembled due to LLM or Retriever initialization failure.")
|
119 |
+
|
120 |
+
# --- Main execution for testing (optional, for direct script run) ---
|
121 |
+
if __name__ == "__main__":
|
122 |
+
print("--- Running main.py for direct test ---")
|
123 |
+
if rag_chain:
|
124 |
+
# Example queries for testing
|
125 |
+
question_1 = "What are the steps for instrument calibration?"
|
126 |
+
print(f"\n--- Executing query: {question_1} ---")
|
127 |
+
response_1 = rag_chain.invoke(question_1) # 直接傳遞字符串給 invoke
|
128 |
+
print("\nLLM's Answer:")
|
129 |
+
print(response_1)
|
130 |
+
|
131 |
+
print("\n--- Executing another query ---")
|
132 |
+
question_2 = "How do I troubleshoot common equipment malfunctions?"
|
133 |
+
response_2 = rag_chain.invoke(question_2) # 直接傳遞字符串給 invoke
|
134 |
+
print("\nLLM's Answer:")
|
135 |
+
print(response_2)
|
136 |
+
|
137 |
+
print("\n--- Executing a query that might not have an answer ---")
|
138 |
+
question_3 = "How to make Tiramisu cake?"
|
139 |
+
response_3 = rag_chain.invoke(question_3) # 直接傳遞字符串給 invoke
|
140 |
+
print("\nLLM's Answer:")
|
141 |
+
print(response_3)
|
142 |
+
else:
|
143 |
+
print("RAG chain is not available for testing. Please ensure 'setup_knowledge_base.py' has been run successfully and check API key/model setup in main.py.")
|
package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
package.json
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "my-app",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"homepage": ".",
|
6 |
+
"dependencies": {
|
7 |
+
"@testing-library/dom": "^10.4.0",
|
8 |
+
"@testing-library/jest-dom": "^6.6.3",
|
9 |
+
"@testing-library/react": "^16.3.0",
|
10 |
+
"@testing-library/user-event": "^13.5.0",
|
11 |
+
"@types/jest": "^27.5.2",
|
12 |
+
"@types/node": "^16.18.126",
|
13 |
+
"@types/react": "^19.1.8",
|
14 |
+
"@types/react-dom": "^19.1.6",
|
15 |
+
"react": "^19.1.0",
|
16 |
+
"react-dom": "^19.1.0",
|
17 |
+
"react-scripts": "5.0.1",
|
18 |
+
"typescript": "^4.9.5",
|
19 |
+
"web-vitals": "^2.1.4",
|
20 |
+
"react-router-dom": "^6.22.3"
|
21 |
+
},
|
22 |
+
"scripts": {
|
23 |
+
"start": "react-scripts start",
|
24 |
+
"build": "react-scripts build",
|
25 |
+
"test": "react-scripts test",
|
26 |
+
"eject": "react-scripts eject"
|
27 |
+
},
|
28 |
+
"eslintConfig": {
|
29 |
+
"extends": [
|
30 |
+
"react-app",
|
31 |
+
"react-app/jest"
|
32 |
+
]
|
33 |
+
},
|
34 |
+
"browserslist": {
|
35 |
+
"production": [
|
36 |
+
">0.2%",
|
37 |
+
"not dead",
|
38 |
+
"not op_mini all"
|
39 |
+
],
|
40 |
+
"development": [
|
41 |
+
"last 1 chrome version",
|
42 |
+
"last 1 firefox version",
|
43 |
+
"last 1 safari version"
|
44 |
+
]
|
45 |
+
}
|
46 |
+
}
|
public/favicon.ico
ADDED
|
public/index.html
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7 |
+
<meta name="theme-color" content="#000000" />
|
8 |
+
<meta
|
9 |
+
name="description"
|
10 |
+
content="Web site created using create-react-app"
|
11 |
+
/>
|
12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
13 |
+
<!--
|
14 |
+
manifest.json provides metadata used when your web app is installed on a
|
15 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
16 |
+
-->
|
17 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
18 |
+
<!--
|
19 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
20 |
+
It will be replaced with the URL of the `public` folder during the build.
|
21 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
22 |
+
|
23 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
24 |
+
work correctly both with client-side routing and a non-root public URL.
|
25 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
26 |
+
-->
|
27 |
+
<title>React App</title>
|
28 |
+
</head>
|
29 |
+
<body>
|
30 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
31 |
+
<div id="root"></div>
|
32 |
+
<!--
|
33 |
+
This HTML file is a template.
|
34 |
+
If you open it directly in the browser, you will see an empty page.
|
35 |
+
|
36 |
+
You can add webfonts, meta tags, or analytics to this file.
|
37 |
+
The build step will place the bundled scripts into the <body> tag.
|
38 |
+
|
39 |
+
To begin the development, run `npm start` or `yarn start`.
|
40 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
41 |
+
-->
|
42 |
+
</body>
|
43 |
+
</html>
|
public/logo192.png
ADDED
![]() |
Git LFS Details
|
public/logo512.png
ADDED
![]() |
Git LFS Details
|
public/manifest.json
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"short_name": "React App",
|
3 |
+
"name": "Create React App Sample",
|
4 |
+
"icons": [
|
5 |
+
{
|
6 |
+
"src": "favicon.ico",
|
7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
8 |
+
"type": "image/x-icon"
|
9 |
+
},
|
10 |
+
{
|
11 |
+
"src": "logo192.png",
|
12 |
+
"type": "image/png",
|
13 |
+
"sizes": "192x192"
|
14 |
+
},
|
15 |
+
{
|
16 |
+
"src": "logo512.png",
|
17 |
+
"type": "image/png",
|
18 |
+
"sizes": "512x512"
|
19 |
+
}
|
20 |
+
],
|
21 |
+
"start_url": ".",
|
22 |
+
"display": "standalone",
|
23 |
+
"theme_color": "#000000",
|
24 |
+
"background_color": "#ffffff"
|
25 |
+
}
|
public/robots.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# https://www.robotstxt.org/robotstxt.html
|
2 |
+
User-agent: *
|
3 |
+
Disallow:
|
requirements.txt
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
pypdf
|
2 |
+
langchain
|
3 |
+
together
|
4 |
+
langchain-together
|
5 |
+
python-dotenv
|
6 |
+
chromadb
|
7 |
+
fastapi
|
8 |
+
uvicorn
|
9 |
+
pydantic
|
10 |
+
python-multipart
|
11 |
+
gradio
|
12 |
+
transformers
|
13 |
+
tokenizers
|
14 |
+
huggingface_hub
|
15 |
+
accelerate
|
16 |
+
peft
|
17 |
+
langchain-community
|
setup_knowledge_base.py
ADDED
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#這個文件將負責所有的數據準備工作。在運行 app.py 之前,你需要先執行這個文件一次。
|
2 |
+
import re
|
3 |
+
import os
|
4 |
+
import sqlite3
|
5 |
+
import datetime
|
6 |
+
import chromadb
|
7 |
+
|
8 |
+
from pypdf import PdfReader
|
9 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
10 |
+
from together import Together
|
11 |
+
from langchain_together import TogetherEmbeddings
|
12 |
+
from dotenv import load_dotenv
|
13 |
+
from chromadb.utils import embedding_functions
|
14 |
+
from langchain_core.documents import Document
|
15 |
+
|
16 |
+
# Load environment variables from .env file
|
17 |
+
load_dotenv()
|
18 |
+
|
19 |
+
def extract_text_from_pdf(pdf_path):
|
20 |
+
"""
|
21 |
+
Extract all text content from PDF files.
|
22 |
+
"""
|
23 |
+
text = ""
|
24 |
+
try:
|
25 |
+
reader = PdfReader(pdf_path)
|
26 |
+
for page in reader.pages:
|
27 |
+
text += page.extract_text() + "\n" # 提取每頁文字並換行
|
28 |
+
except Exception as e:
|
29 |
+
print(f"Error reading PDF {pdf_path}: {e}")
|
30 |
+
return text
|
31 |
+
|
32 |
+
def clean_text_content(text):
|
33 |
+
"""
|
34 |
+
Remove irrelevant content, such as copyright information, legal notices, and long blank spaces.
|
35 |
+
"""
|
36 |
+
text = re.sub(r'\n\s*\n', '\n\n', text)
|
37 |
+
text = re.sub(r'^\s*\d+\s*$', '', text, flags=re.MULTILINE)
|
38 |
+
text = re.sub(r'\s+\d+\s*$', '', text, flags=re.MULTILINE)
|
39 |
+
text = re.sub(r'Copyright © \d{4} [^\n]*\. All Rights Reserved\.', '', text, flags=re.IGNORECASE)
|
40 |
+
text = re.sub(r'Confidential and Proprietary Information[^\n]*', '', text, flags=re.IGNORECASE)
|
41 |
+
text = re.sub(r'Disclaimer:[^.]*\.', '', text, flags=re.IGNORECASE)
|
42 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
43 |
+
return text
|
44 |
+
|
45 |
+
def standardize_text_format(text):
|
46 |
+
"""
|
47 |
+
Try to organize everything into clear paragraphs.
|
48 |
+
"""
|
49 |
+
text = re.sub(r'([a-zA-Z])-(\n)([a-zA-Z])', r'\1\3', text)
|
50 |
+
text = re.sub(r'([.?!])\s*([A-Z])', r'\1 \2', text)
|
51 |
+
text = re.sub(r'\s+', ' ', text).strip()
|
52 |
+
text = re.sub(r'([^\n])\n([^\n])', r'\1 \2', text)
|
53 |
+
text = re.sub(r'\n{2,}', '\n\n', text)
|
54 |
+
return text
|
55 |
+
|
56 |
+
def create_database_table(db_path):
|
57 |
+
"""
|
58 |
+
Create a SQLite database and define the table structure.
|
59 |
+
"""
|
60 |
+
conn = sqlite3.connect(db_path)
|
61 |
+
cursor = conn.cursor()
|
62 |
+
cursor.execute('''
|
63 |
+
CREATE TABLE IF NOT EXISTS documents (
|
64 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
65 |
+
original_filename TEXT NOT NULL UNIQUE, -- Add UNIQUE constraint
|
66 |
+
source_type TEXT,
|
67 |
+
processed_text TEXT NOT NULL,
|
68 |
+
processed_date TEXT
|
69 |
+
)
|
70 |
+
''')
|
71 |
+
conn.commit()
|
72 |
+
conn.close()
|
73 |
+
print(f"SQLite table 'documents' ensured in {db_path}")
|
74 |
+
|
75 |
+
def create_chunks_table(db_path):
|
76 |
+
"""
|
77 |
+
Create a table to store the segmented text blocks.
|
78 |
+
"""
|
79 |
+
conn = sqlite3.connect(db_path)
|
80 |
+
cursor = conn.cursor()
|
81 |
+
cursor.execute('''
|
82 |
+
CREATE TABLE IF NOT EXISTS chunks (
|
83 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
84 |
+
document_id INTEGER NOT NULL,
|
85 |
+
chunk_index INTEGER NOT NULL,
|
86 |
+
chunk_content TEXT NOT NULL,
|
87 |
+
chunk_length INTEGER,
|
88 |
+
created_at TEXT,
|
89 |
+
UNIQUE(document_id, chunk_index), -- Ensure chunks are unique per document
|
90 |
+
FOREIGN KEY (document_id) REFERENCES documents(id)
|
91 |
+
)
|
92 |
+
''')
|
93 |
+
conn.commit()
|
94 |
+
conn.close()
|
95 |
+
print(f"SQLite table 'chunks' ensured in {db_path}")
|
96 |
+
|
97 |
+
def insert_document_data(db_path, original_filename, source_type, processed_text, processed_date):
|
98 |
+
"""
|
99 |
+
Insert the processed file data into the database.
|
100 |
+
"""
|
101 |
+
conn = sqlite3.connect(db_path)
|
102 |
+
cursor = conn.cursor()
|
103 |
+
# 檢查是否已存在相同的原始文件
|
104 |
+
cursor.execute("SELECT id FROM documents WHERE original_filename = ?", (original_filename,))
|
105 |
+
existing_doc = cursor.fetchone()
|
106 |
+
if existing_doc:
|
107 |
+
print(f"Document '{original_filename}' already exists in DB (ID: {existing_doc[0]}). Skipping insertion.")
|
108 |
+
conn.close()
|
109 |
+
return existing_doc[0]
|
110 |
+
else:
|
111 |
+
cursor.execute('''
|
112 |
+
INSERT INTO documents (original_filename, source_type, processed_text, processed_date)
|
113 |
+
VALUES (?, ?, ?, ?)
|
114 |
+
''', (original_filename, source_type, processed_text, processed_date))
|
115 |
+
conn.commit()
|
116 |
+
doc_id = cursor.lastrowid
|
117 |
+
conn.close()
|
118 |
+
print(f"Document '{original_filename}' saved to database: {db_path} with ID: {doc_id}")
|
119 |
+
return doc_id
|
120 |
+
|
121 |
+
def get_document_text_from_db(db_path, document_id=None, limit=None):
|
122 |
+
"""
|
123 |
+
Reads the processed text contents of one or more files from a SQLite database.
|
124 |
+
"""
|
125 |
+
conn = None
|
126 |
+
try:
|
127 |
+
conn = sqlite3.connect(db_path)
|
128 |
+
cursor = conn.cursor()
|
129 |
+
query = "SELECT id, processed_text FROM documents"
|
130 |
+
params = []
|
131 |
+
if document_id:
|
132 |
+
query += " WHERE id = ?"
|
133 |
+
params.append(document_id)
|
134 |
+
if limit:
|
135 |
+
query += " LIMIT ?"
|
136 |
+
params.append(limit)
|
137 |
+
cursor.execute(query, params)
|
138 |
+
if document_id:
|
139 |
+
row = cursor.fetchone()
|
140 |
+
if row:
|
141 |
+
return {'id': row[0], 'processed_text': row[1]}
|
142 |
+
return None
|
143 |
+
else:
|
144 |
+
return [{'id': row[0], 'processed_text': row[1]} for row in cursor.fetchall()]
|
145 |
+
except sqlite3.Error as e:
|
146 |
+
print(f"Error reading from SQLite database: {e}")
|
147 |
+
return []
|
148 |
+
finally:
|
149 |
+
if conn:
|
150 |
+
conn.close()
|
151 |
+
|
152 |
+
def chunk_text(text, chunk_size=500, chunk_overlap=100): # Adjusted chunk size and overlap for potentially smaller chunks
|
153 |
+
"""
|
154 |
+
Splits the text using RecursiveCharacterTextSplitter.
|
155 |
+
"""
|
156 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
157 |
+
chunk_size=chunk_size,
|
158 |
+
chunk_overlap=chunk_overlap,
|
159 |
+
length_function=len,
|
160 |
+
add_start_index=True
|
161 |
+
)
|
162 |
+
chunks = text_splitter.create_documents([text])
|
163 |
+
return [chunk.page_content for chunk in chunks]
|
164 |
+
|
165 |
+
def insert_chunks_to_db(db_path, document_id, chunks):
|
166 |
+
"""
|
167 |
+
Inserts all text blocks from the specified file into the database.
|
168 |
+
Checks if chunks for this document_id already exist and skips if found.
|
169 |
+
"""
|
170 |
+
conn = sqlite3.connect(db_path)
|
171 |
+
cursor = conn.cursor()
|
172 |
+
current_time = datetime.datetime.now().isoformat()
|
173 |
+
|
174 |
+
# Check if chunks for this document_id already exist
|
175 |
+
cursor.execute("SELECT COUNT(*) FROM chunks WHERE document_id = ?", (document_id,))
|
176 |
+
if cursor.fetchone()[0] > 0:
|
177 |
+
print(f"Chunks for document ID {document_id} already exist. Skipping chunk insertion.")
|
178 |
+
conn.close()
|
179 |
+
return
|
180 |
+
|
181 |
+
data_to_insert = []
|
182 |
+
for i, chunk in enumerate(chunks):
|
183 |
+
data_to_insert.append((document_id, i, chunk, len(chunk), current_time))
|
184 |
+
|
185 |
+
cursor.executemany('''
|
186 |
+
INSERT INTO chunks (document_id, chunk_index, chunk_content, chunk_length, created_at)
|
187 |
+
VALUES (?, ?, ?, ?, ?)
|
188 |
+
''', data_to_insert)
|
189 |
+
conn.commit()
|
190 |
+
conn.close()
|
191 |
+
print(f"Saved {len(chunks)} chunks into the database for file ID {document_id}.")
|
192 |
+
|
193 |
+
def generate_embeddings(texts, model_name="togethercomputer/m2-bert-80M-32k-retrieval"):
|
194 |
+
"""
|
195 |
+
Generates text embeddings using Together AI's embedding model.
|
196 |
+
"""
|
197 |
+
try:
|
198 |
+
embeddings_model = TogetherEmbeddings(
|
199 |
+
model=model_name,
|
200 |
+
api_key=os.getenv("TOGETHER_API_KEY")
|
201 |
+
)
|
202 |
+
vectors = embeddings_model.embed_documents(texts)
|
203 |
+
print(f"Successfully generated embeddings for {len(texts)} text chunks using {model_name}.")
|
204 |
+
return vectors
|
205 |
+
except Exception as e:
|
206 |
+
print(f"Error generating embeddings: {e}")
|
207 |
+
return []
|
208 |
+
|
209 |
+
def get_chunks_from_db_for_embedding(db_path):
|
210 |
+
"""
|
211 |
+
Reads all split text chunks from the SQLite database, including their IDs.
|
212 |
+
"""
|
213 |
+
conn = None
|
214 |
+
try:
|
215 |
+
conn = sqlite3.connect(db_path)
|
216 |
+
cursor = conn.cursor()
|
217 |
+
cursor.execute("SELECT id, document_id, chunk_index, chunk_content FROM chunks ORDER BY document_id, chunk_index")
|
218 |
+
rows = cursor.fetchall()
|
219 |
+
return [{'id': row[0], 'source_document_id': row[1], 'chunk_index': row[2], 'text': row[3]} for row in rows]
|
220 |
+
except sqlite3.Error as e:
|
221 |
+
print(f"Error reading chunks from SQLite database: {e}")
|
222 |
+
return []
|
223 |
+
finally:
|
224 |
+
if conn:
|
225 |
+
conn.close()
|
226 |
+
|
227 |
+
def load_chunks_to_vector_db(chunks_data, db_path="vector_db_chroma", collection_name="document_chunks", embeddings_model_name="togethercomputer/m2-bert-80M-32k-retrieval"):
|
228 |
+
"""
|
229 |
+
Loads text chunks and their embeddings into a ChromaDB vector database.
|
230 |
+
This function will now ADD chunks if they are new (based on their IDs).
|
231 |
+
"""
|
232 |
+
try:
|
233 |
+
client = chromadb.PersistentClient(path=db_path)
|
234 |
+
collection = client.get_or_create_collection(name=collection_name)
|
235 |
+
|
236 |
+
# Check existing IDs in ChromaDB to avoid adding duplicates
|
237 |
+
existing_chroma_ids = set()
|
238 |
+
if collection.count() > 0:
|
239 |
+
# Fetching all existing IDs can be slow for very large collections.
|
240 |
+
# A more efficient approach for very large databases might involve
|
241 |
+
# querying a batch of IDs or checking after insertion.
|
242 |
+
# For now, this is simpler for demonstration.
|
243 |
+
try:
|
244 |
+
all_ids_in_chroma = collection.get(ids=collection.get()['ids'])['ids']
|
245 |
+
existing_chroma_ids = set(all_ids_in_chroma)
|
246 |
+
except Exception as e:
|
247 |
+
print(f"Warning: Could not retrieve all existing IDs from ChromaDB. May attempt to add duplicates. Error: {e}")
|
248 |
+
|
249 |
+
|
250 |
+
ids_to_add = []
|
251 |
+
documents_to_add = []
|
252 |
+
embeddings_to_add = []
|
253 |
+
metadatas_to_add = []
|
254 |
+
|
255 |
+
for item in chunks_data:
|
256 |
+
chunk_id_str = str(item['id'])
|
257 |
+
if chunk_id_str not in existing_chroma_ids:
|
258 |
+
ids_to_add.append(chunk_id_str)
|
259 |
+
documents_to_add.append(item['text'])
|
260 |
+
embeddings_to_add.append(item['embedding'])
|
261 |
+
metadatas_to_add.append({"source_document_id": item.get('source_document_id', 'unknown'),
|
262 |
+
"chunk_index": item.get('chunk_index', None)})
|
263 |
+
else:
|
264 |
+
print(f"Chunk ID {chunk_id_str} already exists in ChromaDB. Skipping.")
|
265 |
+
|
266 |
+
if ids_to_add:
|
267 |
+
collection.add(
|
268 |
+
embeddings=embeddings_to_add,
|
269 |
+
documents=documents_to_add,
|
270 |
+
metadatas=metadatas_to_add,
|
271 |
+
ids=ids_to_add
|
272 |
+
)
|
273 |
+
print(f"Successfully added {len(ids_to_add)} new text chunks into ChromaDB collection '{collection_name}'.")
|
274 |
+
else:
|
275 |
+
print(f"No new chunks to add to ChromaDB collection '{collection_name}'.")
|
276 |
+
|
277 |
+
except Exception as e:
|
278 |
+
print(f"Error loading data into ChromaDB: {e}")
|
279 |
+
|
280 |
+
|
281 |
+
if __name__ == "__main__":
|
282 |
+
# Define the directory containing your PDF files
|
283 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
284 |
+
pdf_input_directory = os.path.join(current_dir, "input")
|
285 |
+
#pdf_input_directory = "input" # Ensure this directory exists and contains your PDFs
|
286 |
+
db_directory = "database"
|
287 |
+
db_path = os.path.join(db_directory, "processed_documents.db")
|
288 |
+
vector_db_dir = "vector_db_chroma"
|
289 |
+
collection_name = "my_instrument_manual_chunks"
|
290 |
+
embeddings_model_name = "togethercomputer/m2-bert-80M-32k-retrieval" # 確保與 main.py 中使用的一致
|
291 |
+
|
292 |
+
# Create necessary directories
|
293 |
+
os.makedirs(db_directory, exist_ok=True)
|
294 |
+
os.makedirs(vector_db_dir, exist_ok=True)
|
295 |
+
|
296 |
+
print("--- Starting knowledge base setup ---")
|
297 |
+
|
298 |
+
# 1. 確保 SQLite 表格存在
|
299 |
+
create_database_table(db_path)
|
300 |
+
create_chunks_table(db_path)
|
301 |
+
|
302 |
+
# 2. 遍歷指定目錄下的所有 PDF 文件
|
303 |
+
pdf_files = [f for f in os.listdir(pdf_input_directory) if f.lower().endswith('.pdf')]
|
304 |
+
if not pdf_files:
|
305 |
+
print(f"No PDF files found in '{pdf_input_directory}'. Please place your PDF documents there.")
|
306 |
+
|
307 |
+
for pdf_filename in pdf_files:
|
308 |
+
pdf_full_path = os.path.join(pdf_input_directory, pdf_filename)
|
309 |
+
print(f"\n--- Processing PDF: {pdf_full_path} ---")
|
310 |
+
|
311 |
+
# 檢查文件是否已在 documents 表中處理過
|
312 |
+
conn = sqlite3.connect(db_path)
|
313 |
+
cursor = conn.cursor()
|
314 |
+
cursor.execute("SELECT id FROM documents WHERE original_filename = ?", (pdf_filename,))
|
315 |
+
existing_doc_id = cursor.fetchone()
|
316 |
+
conn.close()
|
317 |
+
|
318 |
+
if existing_doc_id:
|
319 |
+
doc_id = existing_doc_id[0]
|
320 |
+
print(f"Document '{pdf_filename}' already in 'documents' table with ID: {doc_id}. Skipping PDF extraction and text processing.")
|
321 |
+
# Still process chunks if they aren't in 'chunks' table or ChromaDB
|
322 |
+
else:
|
323 |
+
pdf_content = extract_text_from_pdf(pdf_full_path)
|
324 |
+
if pdf_content:
|
325 |
+
cleaned_text = clean_text_content(pdf_content)
|
326 |
+
final_processed_text = standardize_text_format(cleaned_text)
|
327 |
+
current_date = datetime.date.today().isoformat()
|
328 |
+
doc_id = insert_document_data(db_path, pdf_filename, "PDF", final_processed_text, current_date)
|
329 |
+
else:
|
330 |
+
print(f"No content extracted from {pdf_full_path}. Skipping document insertion for this PDF.")
|
331 |
+
continue # Move to the next PDF if no content
|
332 |
+
|
333 |
+
# 如果 doc_id 存在(無論是新插入還是已存在的),則進行分塊和嵌入處理
|
334 |
+
if doc_id is not None:
|
335 |
+
# 從 SQLite documents 表讀取並分塊,存儲到 SQLite chunks 表
|
336 |
+
document_from_db = get_document_text_from_db(db_path, document_id=doc_id)
|
337 |
+
if document_from_db:
|
338 |
+
full_text = document_from_db['processed_text']
|
339 |
+
print(f"--- Chunking document ID {doc_id} ('{pdf_filename}') ---")
|
340 |
+
chunks = chunk_text(full_text, chunk_size=500, chunk_overlap=100) # Re-evaluate chunk_size
|
341 |
+
if chunks:
|
342 |
+
insert_chunks_to_db(db_path, doc_id, chunks)
|
343 |
+
else:
|
344 |
+
print(f"Document ID {doc_id} unable to split any chunks.")
|
345 |
+
else:
|
346 |
+
print(f"Could not retrieve processed text for document ID {doc_id}.")
|
347 |
+
|
348 |
+
# 3. 從 SQLite chunks 表讀取所有塊並生成嵌入,載入到 ChromaDB
|
349 |
+
# 我們需要重新從 DB 獲取所有 chunks,因為可能有多個 PDF 的 chunks
|
350 |
+
chunks_from_db_for_embedding = get_chunks_from_db_for_embedding(db_path)
|
351 |
+
|
352 |
+
if chunks_from_db_for_embedding:
|
353 |
+
# Filter out chunks that already have an embedding in ChromaDB if checking was efficient
|
354 |
+
# For simplicity, we'll try to generate embeddings for all chunks retrieved from SQLite
|
355 |
+
# and let load_chunks_to_vector_db handle duplicates in ChromaDB.
|
356 |
+
texts_to_embed = [item['text'] for item in chunks_from_db_for_embedding]
|
357 |
+
print(f"\n--- Generating embeddings for {len(texts_to_embed)} total chunks ---")
|
358 |
+
text_embeddings = generate_embeddings(texts_to_embed, model_name=embeddings_model_name)
|
359 |
+
|
360 |
+
if text_embeddings:
|
361 |
+
data_for_vector_db = []
|
362 |
+
for i, chunk_data in enumerate(chunks_from_db_for_embedding):
|
363 |
+
if i < len(text_embeddings):
|
364 |
+
chunk_data['embedding'] = text_embeddings[i]
|
365 |
+
data_for_vector_db.append(chunk_data)
|
366 |
+
else:
|
367 |
+
print(f"Warning: Missing embedding for chunk {chunk_data['id']}. Skipping this chunk for ChromaDB.")
|
368 |
+
|
369 |
+
print(f"--- Loading/Updating {len(data_for_vector_db)} chunks into ChromaDB ---")
|
370 |
+
load_chunks_to_vector_db(data_for_vector_db, db_path=vector_db_dir, collection_name=collection_name, embeddings_model_name=embeddings_model_name)
|
371 |
+
else:
|
372 |
+
print("No embeddings generated. Skipping ChromaDB loading.")
|
373 |
+
else:
|
374 |
+
print("No text chunks read from the database for embedding and loading into ChromaDB.")
|
375 |
+
|
376 |
+
print("\n--- Knowledge base setup complete ---")
|
src/App.css
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.App {
|
2 |
+
text-align: center;
|
3 |
+
}
|
4 |
+
|
5 |
+
.App-logo {
|
6 |
+
height: 40vmin;
|
7 |
+
pointer-events: none;
|
8 |
+
}
|
9 |
+
|
10 |
+
@media (prefers-reduced-motion: no-preference) {
|
11 |
+
.App-logo {
|
12 |
+
animation: App-logo-spin infinite 20s linear;
|
13 |
+
}
|
14 |
+
}
|
15 |
+
|
16 |
+
.App-header {
|
17 |
+
background-color: #282c34;
|
18 |
+
min-height: 100vh;
|
19 |
+
display: flex;
|
20 |
+
flex-direction: column;
|
21 |
+
align-items: center;
|
22 |
+
justify-content: center;
|
23 |
+
font-size: calc(10px + 2vmin);
|
24 |
+
color: white;
|
25 |
+
}
|
26 |
+
|
27 |
+
.App-link {
|
28 |
+
color: #61dafb;
|
29 |
+
}
|
30 |
+
|
31 |
+
@keyframes App-logo-spin {
|
32 |
+
from {
|
33 |
+
transform: rotate(0deg);
|
34 |
+
}
|
35 |
+
to {
|
36 |
+
transform: rotate(360deg);
|
37 |
+
}
|
38 |
+
}
|
39 |
+
|
40 |
+
/* Landing Page Styles */
|
41 |
+
.landing-container { font-family: Arial, sans-serif; min-height: 100vh; display: flex; flex-direction: column; }
|
42 |
+
.landing-header, .landing-footer { background: #f8f9fb; padding: 20px 40px; display: flex; justify-content: space-between; align-items: center; }
|
43 |
+
.landing-main { flex: 1; padding: 40px; display: flex; flex-direction: column; align-items: center; }
|
44 |
+
.landing-header .logo { font-weight: bold; font-size: 1.5rem; }
|
45 |
+
.landing-nav a, .lang-select { margin: 0 10px; text-decoration: none; color: #222; }
|
46 |
+
.start-btn { background: #2563eb; color: #fff; border: none; padding: 14px 32px; border-radius: 8px; font-size: 1.1rem; margin: 24px 0; cursor: pointer; }
|
47 |
+
.how-it-works { margin: 40px 0; width: 100%; max-width: 900px; }
|
48 |
+
.how-it-works h2 {text-align: center; }
|
49 |
+
.steps { display: flex; justify-content: space-between; }
|
50 |
+
.step { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #e5e7eb; padding: 24px; flex: 1; margin: 0 10px; text-align: center; }
|
51 |
+
.step-number { background: #2563eb; color: #fff; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 1.2rem; }
|
52 |
+
.download-resources { margin: 40px 0; width: 100%; max-width: 900px; }
|
53 |
+
.download-resources h2 {text-align: center; }
|
54 |
+
.resources { display: flex; justify-content: space-between; }
|
55 |
+
.resource-card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px #e5e7eb; padding: 24px; flex: 1; margin: 0 10px; text-align: center; }
|
56 |
+
.view-all { display: block; margin-top: 16px; text-align: right; color: #2563eb; text-decoration: none; }
|
57 |
+
|
58 |
+
/* Chat Page Styles */
|
59 |
+
.chat-container { min-height: 100vh; display: flex; flex-direction: column; background: #f4f6fa; }
|
60 |
+
.chat-header { background: #2563eb; color: #fff; padding: 20px; font-size: 1.3rem; text-align: center; }
|
61 |
+
.chat-messages { flex: 1; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; }
|
62 |
+
.chat-message { max-width: 60%; padding: 12px 18px; border-radius: 16px; margin-bottom: 8px; }
|
63 |
+
.chat-message.user { align-self: flex-end; background: #2563eb; color: #fff; }
|
64 |
+
.chat-message.ai { align-self: flex-start; background: #fff; color: #222; border: 1px solid #e5e7eb; }
|
65 |
+
.chat-input-area { display: flex; padding: 16px; background: #fff; border-top: 1px solid #e5e7eb; }
|
66 |
+
.chat-input-area input { flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #e5e7eb; margin-right: 10px; font-size: 1rem; }
|
67 |
+
.chat-input-area button { background: #2563eb; color: #fff; border: none; border-radius: 8px; padding: 10px 24px; font-size: 1rem; cursor: pointer; }
|
src/App.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { Routes, Route } from 'react-router-dom';
|
3 |
+
import LandingPage from './LandingPage';
|
4 |
+
import ChatPage from './ChatPage';
|
5 |
+
import './App.css';
|
6 |
+
import './foot.css';
|
7 |
+
|
8 |
+
function App() {
|
9 |
+
return (
|
10 |
+
<Routes>
|
11 |
+
<Route path="/" element={<LandingPage />} />
|
12 |
+
<Route path="/chat" element={<ChatPage />} />
|
13 |
+
</Routes>
|
14 |
+
);
|
15 |
+
}
|
16 |
+
|
17 |
+
export default App;
|
18 |
+
|
19 |
+
|
src/ChatPage.css
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.chat-container {
|
2 |
+
display: flex;
|
3 |
+
flex-direction: column;
|
4 |
+
height: 100vh;
|
5 |
+
max-width: 800px;
|
6 |
+
margin: auto;
|
7 |
+
font-family: Arial, sans-serif;
|
8 |
+
}
|
9 |
+
|
10 |
+
.chat-header {
|
11 |
+
padding: 16px;
|
12 |
+
background-color: #4a90e2;
|
13 |
+
color: white;
|
14 |
+
font-size: 1.5rem;
|
15 |
+
text-align: center;
|
16 |
+
}
|
17 |
+
|
18 |
+
.chat-messages {
|
19 |
+
flex: 1;
|
20 |
+
padding: 16px;
|
21 |
+
overflow-y: auto;
|
22 |
+
background-color: #f5f5f5;
|
23 |
+
display: flex;
|
24 |
+
flex-direction: column;
|
25 |
+
gap: 12px;
|
26 |
+
}
|
27 |
+
|
28 |
+
.chat-bubble {
|
29 |
+
max-width: 70%;
|
30 |
+
padding: 12px;
|
31 |
+
border-radius: 16px;
|
32 |
+
white-space: pre-wrap;
|
33 |
+
word-wrap: break-word;
|
34 |
+
}
|
35 |
+
|
36 |
+
.chat-bubble.user {
|
37 |
+
align-self: flex-end;
|
38 |
+
background-color: #dcf8c6;
|
39 |
+
}
|
40 |
+
|
41 |
+
.chat-bubble.ai {
|
42 |
+
align-self: flex-start;
|
43 |
+
background-color: #ffffff;
|
44 |
+
border: 1px solid #ddd;
|
45 |
+
}
|
46 |
+
|
47 |
+
.chat-bubble.loading {
|
48 |
+
font-style: italic;
|
49 |
+
opacity: 0.6;
|
50 |
+
}
|
51 |
+
|
52 |
+
.chat-input-area {
|
53 |
+
display: flex;
|
54 |
+
padding: 12px;
|
55 |
+
border-top: 1px solid #ccc;
|
56 |
+
background-color: #fff;
|
57 |
+
}
|
58 |
+
|
59 |
+
.chat-input-area input {
|
60 |
+
flex: 1;
|
61 |
+
padding: 8px;
|
62 |
+
border: 1px solid #ccc;
|
63 |
+
border-radius: 8px;
|
64 |
+
margin-right: 8px;
|
65 |
+
}
|
66 |
+
|
67 |
+
.chat-input-area button {
|
68 |
+
padding: 8px 16px;
|
69 |
+
background-color: #4a90e2;
|
70 |
+
color: white;
|
71 |
+
border: none;
|
72 |
+
border-radius: 8px;
|
73 |
+
cursor: pointer;
|
74 |
+
}
|
75 |
+
|
76 |
+
.chat-input-area button:disabled {
|
77 |
+
background-color: #a0c4f2;
|
78 |
+
cursor: not-allowed;
|
79 |
+
}
|
80 |
+
|
81 |
+
.back-button {
|
82 |
+
float: right;
|
83 |
+
margin-right: 20px;
|
84 |
+
background-color: #f2f2f2;
|
85 |
+
border: none;
|
86 |
+
padding: 6px 12px;
|
87 |
+
border-radius: 8px;
|
88 |
+
cursor: pointer;
|
89 |
+
font-size: 14px;
|
90 |
+
}
|
91 |
+
|
92 |
+
.back-button:hover {
|
93 |
+
background-color: #ddd;
|
94 |
+
}
|
src/ChatPage.tsx
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import './ChatPage.css'; // 確保你有這個樣式檔案
|
3 |
+
import { useNavigate } from 'react-router-dom';
|
4 |
+
|
5 |
+
// frontend message type
|
6 |
+
interface Message {
|
7 |
+
sender: 'user' | 'ai';
|
8 |
+
text: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
// Type definition of backend return data
|
12 |
+
interface AskResponse {
|
13 |
+
answer: string;
|
14 |
+
source_documents: string[];
|
15 |
+
}
|
16 |
+
|
17 |
+
const ChatPage: React.FC = () => {
|
18 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
19 |
+
const [input, setInput] = useState('');
|
20 |
+
const [loading, setLoading] = useState(false);
|
21 |
+
const navigate = useNavigate();
|
22 |
+
|
23 |
+
const handleSend = async () => {
|
24 |
+
if (input.trim() === '') return;
|
25 |
+
|
26 |
+
const userMessage: Message = { sender: 'user', text: input };
|
27 |
+
setMessages((prev) => [...prev, userMessage]);
|
28 |
+
setInput('');
|
29 |
+
setLoading(true);
|
30 |
+
|
31 |
+
try {
|
32 |
+
|
33 |
+
const res = await fetch('/ask', { // 改成相對路徑
|
34 |
+
method: 'POST',
|
35 |
+
headers: { 'Content-Type': 'application/json' },
|
36 |
+
body: JSON.stringify({ query: input }),
|
37 |
+
});
|
38 |
+
|
39 |
+
|
40 |
+
if (!res.ok) throw new Error('Backend error');
|
41 |
+
|
42 |
+
const data: AskResponse = await res.json();
|
43 |
+
|
44 |
+
const aiText = `${data.answer}\n\n📚 Source Snippets:\n${data.source_documents.slice(0, 2).join('\n\n')}`;
|
45 |
+
const aiMessage: Message = { sender: 'ai', text: aiText };
|
46 |
+
|
47 |
+
setMessages((prev) => [...prev, aiMessage]);
|
48 |
+
} catch (err) {
|
49 |
+
console.error('Fetch error:', err);
|
50 |
+
const errorMessage: Message = { sender: 'ai', text: '❌ Unable to get a response, please try again later.' };
|
51 |
+
setMessages((prev) => [...prev, errorMessage]);
|
52 |
+
} finally {
|
53 |
+
setLoading(false);
|
54 |
+
}
|
55 |
+
};
|
56 |
+
|
57 |
+
return (
|
58 |
+
<div className="chat-container">
|
59 |
+
<div className="back-button-container">
|
60 |
+
<button className="back-button" onClick={() => navigate('/')}>
|
61 |
+
🔙 Back to Home Page
|
62 |
+
</button>
|
63 |
+
</div>
|
64 |
+
<header className="chat-header">LabAid AI Chat</header>
|
65 |
+
<div className="chat-messages">
|
66 |
+
{messages.map((msg, idx) => (
|
67 |
+
<div key={idx} className={`chat-bubble ${msg.sender}`}>
|
68 |
+
{msg.text}
|
69 |
+
</div>
|
70 |
+
))}
|
71 |
+
{loading && <div className="chat-bubble ai loading">💬 thinking...</div>}
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<div className="chat-input-area">
|
75 |
+
<input
|
76 |
+
type="text"
|
77 |
+
value={input}
|
78 |
+
onChange={(e) => setInput(e.target.value)}
|
79 |
+
onKeyDown={(e) => {
|
80 |
+
if (e.key === 'Enter') handleSend();
|
81 |
+
}}
|
82 |
+
placeholder="please enter your question..."
|
83 |
+
/>
|
84 |
+
<button onClick={handleSend} disabled={loading}>send</button>
|
85 |
+
</div>
|
86 |
+
</div>
|
87 |
+
);
|
88 |
+
};
|
89 |
+
|
90 |
+
export default ChatPage;
|
src/LandingPage.tsx
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { useNavigate } from 'react-router-dom';
|
3 |
+
|
4 |
+
|
5 |
+
|
6 |
+
const LandingPage: React.FC = () => {
|
7 |
+
const navigate = useNavigate();
|
8 |
+
|
9 |
+
return (
|
10 |
+
<div className="landing-container">
|
11 |
+
<header className="landing-header">
|
12 |
+
<div className="logo">LabAid AI</div>
|
13 |
+
<nav className="landing-nav">
|
14 |
+
<a href="#features">Features</a>
|
15 |
+
<a href="#how-it-works">How It Works</a>
|
16 |
+
<a href="#pricing">Pricing</a>
|
17 |
+
<span className="lang-select">English</span>
|
18 |
+
</nav>
|
19 |
+
</header>
|
20 |
+
<main className="landing-main">
|
21 |
+
<h1>AI-Powered Lab <span className="highlight">Troubleshooting</span></h1>
|
22 |
+
<p>Instantly diagnose and resolve equipment issues with our intelligent AI assistant. Get step-by-step solutions, maintenance tips, and expert guidance 24/7.</p>
|
23 |
+
<button className="start-btn" onClick={() => navigate('/chat')}>Start Troubleshooting</button>
|
24 |
+
<section className="how-it-works" id="how-it-works">
|
25 |
+
<h2>How It Works</h2>
|
26 |
+
<div className="steps">
|
27 |
+
<div className="step">
|
28 |
+
<div className="step-number">1</div>
|
29 |
+
<h3>Describe the Issue</h3>
|
30 |
+
<p>Simply tell our AI what's wrong with your equipment. Use natural language - no technical jargon required.</p>
|
31 |
+
</div>
|
32 |
+
<div className="step">
|
33 |
+
<div className="step-number">2</div>
|
34 |
+
<h3>Get AI Analysis</h3>
|
35 |
+
<p>Our advanced AI analyzes your description and equipment type to provide accurate diagnosis and solutions.</p>
|
36 |
+
</div>
|
37 |
+
<div className="step">
|
38 |
+
<div className="step-number">3</div>
|
39 |
+
<h3>Follow the Steps</h3>
|
40 |
+
<p>Receive detailed, step-by-step instructions with visual aids to resolve the issue quickly and safely.</p>
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
</section>
|
44 |
+
<section className="download-resources">
|
45 |
+
<h2>Download Resources</h2>
|
46 |
+
<div className="resources">
|
47 |
+
<div className="resource-card">
|
48 |
+
<h3>Equipment Manual</h3>
|
49 |
+
<p>Complete user manual for lab equipment operation and safety guidelines.</p>
|
50 |
+
<span>PDF · 2.4 MB</span>
|
51 |
+
<button>Download</button>
|
52 |
+
</div>
|
53 |
+
<div className="resource-card">
|
54 |
+
<h3>Troubleshooting Guide</h3>
|
55 |
+
<p>Step-by-step solutions for common equipment issues and error codes.</p>
|
56 |
+
<span>PDF · 1.8 MB</span>
|
57 |
+
<button>Download</button>
|
58 |
+
</div>
|
59 |
+
<div className="resource-card">
|
60 |
+
<h3>Maintenance Schedule</h3>
|
61 |
+
<p>Recommended maintenance timeline and procedures for optimal performance.</p>
|
62 |
+
<span>PDF · 0.9 MB</span>
|
63 |
+
<button>Download</button>
|
64 |
+
</div>
|
65 |
+
</div>
|
66 |
+
<a href="#all-documents" className="view-all">View All Documents →</a>
|
67 |
+
</section>
|
68 |
+
</main>
|
69 |
+
<footer className="labassist-footer">
|
70 |
+
<div className="footer-main-content">
|
71 |
+
<div className="footer-column company-info">
|
72 |
+
<div className="company-logo-section">
|
73 |
+
|
74 |
+
<span>LabAid AI</span>
|
75 |
+
</div>
|
76 |
+
<p>Revolutionizing lab equipment troubleshooting with AI-powered solutions.</p>
|
77 |
+
<div className="social-icons">
|
78 |
+
|
79 |
+
</div>
|
80 |
+
</div>
|
81 |
+
<div className="footer-column">
|
82 |
+
<h4>Product</h4>
|
83 |
+
<ul>
|
84 |
+
<li><a href="#">Features</a></li>
|
85 |
+
<li><a href="#">Pricing</a></li>
|
86 |
+
<li><a href="#">API</a></li>
|
87 |
+
<li><a href="#">Documentation</a></li>
|
88 |
+
</ul>
|
89 |
+
</div>
|
90 |
+
<div className="footer-column">
|
91 |
+
<h4>Support</h4>
|
92 |
+
<ul>
|
93 |
+
<li><a href="#">Help Center</a></li>
|
94 |
+
<li><a href="#">Contact Us</a></li>
|
95 |
+
<li><a href="#">Status</a></li>
|
96 |
+
<li><a href="#">Community</a></li>
|
97 |
+
</ul>
|
98 |
+
</div>
|
99 |
+
<div className="footer-column">
|
100 |
+
<h4>Company</h4>
|
101 |
+
<ul>
|
102 |
+
<li><a href="#">About</a></li>
|
103 |
+
<li><a href="#">Blog</a></li>
|
104 |
+
<li><a href="#">Careers</a></li>
|
105 |
+
<li><a href="#">Press</a></li>
|
106 |
+
</ul>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
<div className="footer-bottom-bar">
|
110 |
+
<p>© 2024 LabAssist AI. All rights reserved.</p>
|
111 |
+
<div className="footer-legal-links">
|
112 |
+
<a href="#">Privacy Policy</a>
|
113 |
+
<a href="#">Terms of Service</a>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
</footer>
|
117 |
+
</div>
|
118 |
+
|
119 |
+
|
120 |
+
);
|
121 |
+
};
|
122 |
+
|
123 |
+
export default LandingPage;
|
src/foot.css
ADDED
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.labassist-footer {
|
2 |
+
background-color: #111122; /* 頁腳背景色,比 body 略深 */
|
3 |
+
padding: 60px 40px; /* 上下左右內邊距 */
|
4 |
+
display: flex;
|
5 |
+
flex-direction: column; /* 讓主要內容和底部版權資訊垂直堆疊 */
|
6 |
+
align-items: center; /* 讓整個 footer 的內容水平居中(在有 max-width 的情況下) */
|
7 |
+
}
|
8 |
+
|
9 |
+
/* 主要內容區域:包含四個列 */
|
10 |
+
.footer-main-content {
|
11 |
+
display: flex;
|
12 |
+
justify-content: space-between; /* 將四個列均勻分佈在水平空間 */
|
13 |
+
width: 100%; /* 佔滿父容器寬度 */
|
14 |
+
max-width: 1200px; /* 限制內容的最大寬度,使其不會太寬 */
|
15 |
+
margin-bottom: 50px; /* 與底部版權資訊的間距 */
|
16 |
+
flex-wrap: wrap; /* 允許在小螢幕上換行 */
|
17 |
+
}
|
18 |
+
|
19 |
+
/* 每一個列(公司資訊、Product、Support、Company) */
|
20 |
+
.footer-column {
|
21 |
+
flex-basis: 22%; /* 讓每列佔據大約 22% 的空間,以便容納 4 列並留有間距 */
|
22 |
+
/* 可以根據內容調整 flex-basis,或者使用 flex-grow: 1; 讓它們自動分配空間 */
|
23 |
+
margin-bottom: 20px; /* 小螢幕換行時的間距 */
|
24 |
+
}
|
25 |
+
|
26 |
+
.footer-column h4 {
|
27 |
+
font-size: 1.1em;
|
28 |
+
color: #FFFFFF; /* 標題文字白色 */
|
29 |
+
margin-bottom: 15px; /* 標題與下方連結的間距 */
|
30 |
+
font-weight: bold;
|
31 |
+
}
|
32 |
+
|
33 |
+
.footer-column ul {
|
34 |
+
list-style: none; /* 移除列表點 */
|
35 |
+
}
|
36 |
+
|
37 |
+
.footer-column ul li {
|
38 |
+
margin-bottom: 8px; /* 連結之間的間距 */
|
39 |
+
}
|
40 |
+
|
41 |
+
.footer-column ul li a {
|
42 |
+
color: #B0B0B0; /* 連結文字顏色 */
|
43 |
+
text-decoration: none; /* 移除下劃線 */
|
44 |
+
font-size: 0.95em;
|
45 |
+
transition: color 0.3s ease; /* 平滑過渡效果 */
|
46 |
+
}
|
47 |
+
|
48 |
+
.footer-column ul li a:hover {
|
49 |
+
color: #FFFFFF; /* 鼠標懸停時變白 */
|
50 |
+
}
|
51 |
+
|
52 |
+
/* 左側公司資訊區塊特有樣式 */
|
53 |
+
.company-info {
|
54 |
+
flex-basis: 30%; /* 給公司資訊列更多空間 */
|
55 |
+
min-width: 250px; /* 確保在小螢幕上有足夠寬度 */
|
56 |
+
}
|
57 |
+
|
58 |
+
.company-logo-section {
|
59 |
+
display: flex;
|
60 |
+
align-items: center;
|
61 |
+
margin-bottom: 15px;
|
62 |
+
}
|
63 |
+
|
64 |
+
.footer-logo {
|
65 |
+
height: 30px; /* Logo 高度 */
|
66 |
+
margin-right: 10px;
|
67 |
+
}
|
68 |
+
|
69 |
+
.company-logo-section span {
|
70 |
+
font-size: 1.2em;
|
71 |
+
font-weight: bold;
|
72 |
+
color: #FFFFFF;
|
73 |
+
}
|
74 |
+
|
75 |
+
.company-info p {
|
76 |
+
font-size: 0.9em;
|
77 |
+
line-height: 1.6;
|
78 |
+
margin-bottom: 20px;
|
79 |
+
color: #B0B0B0;
|
80 |
+
}
|
81 |
+
|
82 |
+
.social-icons {
|
83 |
+
display: flex;
|
84 |
+
gap: 15px; /* 圖標間距 */
|
85 |
+
}
|
86 |
+
|
87 |
+
.social-icons a img {
|
88 |
+
width: 20px; /* 圖標大小 */
|
89 |
+
height: 20px;
|
90 |
+
filter: invert(100%) brightness(80%); /* 將圖標顏色反轉為淺色 */
|
91 |
+
transition: filter 0.3s ease;
|
92 |
+
}
|
93 |
+
|
94 |
+
.social-icons a:hover img {
|
95 |
+
filter: invert(100%) brightness(100%); /* 鼠標懸停時變亮 */
|
96 |
+
}
|
97 |
+
|
98 |
+
|
99 |
+
/* 底部版權資訊區域 */
|
100 |
+
.footer-bottom-bar {
|
101 |
+
border-top: 1px solid #333344; /* 上方加一條細分隔線 */
|
102 |
+
padding-top: 30px;
|
103 |
+
width: 100%;
|
104 |
+
max-width: 1200px; /* 與主要內容寬度保持一致 */
|
105 |
+
display: flex;
|
106 |
+
justify-content: space-between; /* 版權資訊靠左,法律連結靠右 */
|
107 |
+
align-items: center; /* 垂直居中對齊 */
|
108 |
+
font-size: 0.85em;
|
109 |
+
color: #888899; /* 較淺的灰色 */
|
110 |
+
}
|
111 |
+
|
112 |
+
.footer-legal-links a {
|
113 |
+
color: #888899;
|
114 |
+
text-decoration: none;
|
115 |
+
margin-left: 20px; /* 連結之間的間距 */
|
116 |
+
transition: color 0.3s ease;
|
117 |
+
}
|
118 |
+
|
119 |
+
.footer-legal-links a:hover {
|
120 |
+
color: #FFFFFF;
|
121 |
+
}
|
122 |
+
|
123 |
+
/* 響應式設計:小螢幕適應 */
|
124 |
+
@media (max-width: 768px) {
|
125 |
+
.labassist-footer {
|
126 |
+
padding: 40px 20px;
|
127 |
+
}
|
128 |
+
|
129 |
+
.footer-main-content {
|
130 |
+
flex-direction: column; /* 小螢幕時,列變成垂直堆疊 */
|
131 |
+
align-items: flex-start; /* 讓每個列都靠左對齊 */
|
132 |
+
gap: 30px; /* 列之間的間距 */
|
133 |
+
}
|
134 |
+
|
135 |
+
.footer-column {
|
136 |
+
flex-basis: 100%; /* 每列佔據全部寬度 */
|
137 |
+
max-width: 100%; /* 確保最大寬度 */
|
138 |
+
}
|
139 |
+
|
140 |
+
.footer-bottom-bar {
|
141 |
+
flex-direction: column; /* 版權和法律連結垂直堆疊 */
|
142 |
+
text-align: center;
|
143 |
+
}
|
144 |
+
|
145 |
+
.footer-legal-links {
|
146 |
+
margin-top: 15px; /* 與版權資訊的間距 */
|
147 |
+
}
|
148 |
+
|
149 |
+
.footer-legal-links a {
|
150 |
+
margin: 0 10px; /* 調整連結間距 */
|
151 |
+
}
|
152 |
+
}
|
153 |
+
|
154 |
+
@media (max-width: 480px) {
|
155 |
+
.labassist-footer {
|
156 |
+
padding: 30px 15px;
|
157 |
+
}
|
158 |
+
|
159 |
+
.footer-main-content {
|
160 |
+
gap: 20px;
|
161 |
+
}
|
162 |
+
|
163 |
+
.social-icons {
|
164 |
+
gap: 10px;
|
165 |
+
}
|
166 |
+
}
|
src/index.css
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
body {
|
2 |
+
margin: 0;
|
3 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
4 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
5 |
+
sans-serif;
|
6 |
+
-webkit-font-smoothing: antialiased;
|
7 |
+
-moz-osx-font-smoothing: grayscale;
|
8 |
+
}
|
9 |
+
|
10 |
+
code {
|
11 |
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
12 |
+
monospace;
|
13 |
+
}
|
src/index.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import ReactDOM from 'react-dom/client';
|
3 |
+
import './index.css';
|
4 |
+
import App from './App';
|
5 |
+
import reportWebVitals from './reportWebVitals';
|
6 |
+
import { HashRouter } from 'react-router-dom';
|
7 |
+
|
8 |
+
const root = ReactDOM.createRoot(
|
9 |
+
document.getElementById('root') as HTMLElement
|
10 |
+
);
|
11 |
+
|
12 |
+
root.render(
|
13 |
+
<React.StrictMode>
|
14 |
+
<HashRouter>
|
15 |
+
<App />
|
16 |
+
</HashRouter>
|
17 |
+
</React.StrictMode>
|
18 |
+
);
|
19 |
+
|
20 |
+
reportWebVitals();
|
src/logo.svg
ADDED
|
src/picture/linkedin-icon.png
ADDED
![]() |
Git LFS Details
|
src/picture/logo.png
ADDED
![]() |
Git LFS Details
|
src/react-app-env.d.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
/// <reference types="react-scripts" />
|
src/reportWebVitals.ts
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReportHandler } from 'web-vitals';
|
2 |
+
|
3 |
+
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
4 |
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
5 |
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
6 |
+
getCLS(onPerfEntry);
|
7 |
+
getFID(onPerfEntry);
|
8 |
+
getFCP(onPerfEntry);
|
9 |
+
getLCP(onPerfEntry);
|
10 |
+
getTTFB(onPerfEntry);
|
11 |
+
});
|
12 |
+
}
|
13 |
+
};
|
14 |
+
|
15 |
+
export default reportWebVitals;
|
src/setupTests.ts
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
2 |
+
// allows you to do things like:
|
3 |
+
// expect(element).toHaveTextContent(/react/i)
|
4 |
+
// learn more: https://github.com/testing-library/jest-dom
|
5 |
+
import '@testing-library/jest-dom';
|
tsconfig.json
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"target": "es5",
|
4 |
+
"lib": [
|
5 |
+
"dom",
|
6 |
+
"dom.iterable",
|
7 |
+
"esnext"
|
8 |
+
],
|
9 |
+
"allowJs": true,
|
10 |
+
"skipLibCheck": true,
|
11 |
+
"esModuleInterop": true,
|
12 |
+
"allowSyntheticDefaultImports": true,
|
13 |
+
"strict": true,
|
14 |
+
"forceConsistentCasingInFileNames": true,
|
15 |
+
"noFallthroughCasesInSwitch": true,
|
16 |
+
"module": "esnext",
|
17 |
+
"moduleResolution": "node",
|
18 |
+
"resolveJsonModule": true,
|
19 |
+
"isolatedModules": true,
|
20 |
+
"noEmit": true,
|
21 |
+
"jsx": "react-jsx"
|
22 |
+
},
|
23 |
+
"include": [
|
24 |
+
"src"
|
25 |
+
]
|
26 |
+
}
|
vector_db_chroma/chroma.sqlite3
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a2d0d855229b7e7711de0cecbee3f1b2214b1b4b9a84f9e6a47510907654a0ae
|
3 |
+
size 7811072
|
vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/data_level0.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:23add52afbe7588391f32d3deffb581b2663d2e2ad8851aba7de25e6b3f66761
|
3 |
+
size 32120000
|
vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/header.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:f8c7f00b4415698ee6cb94332eff91aedc06ba8e066b1f200e78ca5df51abb57
|
3 |
+
size 100
|
vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/length.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:29382d806e774618a2c3512be096526ba7c53fe3fcfb120b10c4f353accbad9f
|
3 |
+
size 40000
|