J266501 commited on
Commit
effe6e2
·
1 Parent(s): 2eb52ba
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +25 -0
  2. README.md +13 -0
  3. api_service.py +145 -0
  4. app.py +6 -0
  5. build/asset-manifest.json +15 -0
  6. build/favicon.ico +0 -0
  7. build/index.html +1 -0
  8. build/logo192.png +3 -0
  9. build/logo512.png +3 -0
  10. build/manifest.json +25 -0
  11. build/robots.txt +3 -0
  12. build/static/css/main.1eeb7222.css +2 -0
  13. build/static/css/main.1eeb7222.css.map +1 -0
  14. build/static/js/453.ed3810f9.chunk.js +2 -0
  15. build/static/js/453.ed3810f9.chunk.js.map +1 -0
  16. build/static/js/main.d1af9f99.js +0 -0
  17. build/static/js/main.d1af9f99.js.LICENSE.txt +71 -0
  18. build/static/js/main.d1af9f99.js.map +0 -0
  19. database/processed_documents.db +3 -0
  20. dockerfile +34 -0
  21. main.py +143 -0
  22. package-lock.json +0 -0
  23. package.json +46 -0
  24. public/favicon.ico +0 -0
  25. public/index.html +43 -0
  26. public/logo192.png +3 -0
  27. public/logo512.png +3 -0
  28. public/manifest.json +25 -0
  29. public/robots.txt +3 -0
  30. requirements.txt +17 -0
  31. setup_knowledge_base.py +376 -0
  32. src/App.css +67 -0
  33. src/App.tsx +19 -0
  34. src/ChatPage.css +94 -0
  35. src/ChatPage.tsx +90 -0
  36. src/LandingPage.tsx +123 -0
  37. src/foot.css +166 -0
  38. src/index.css +13 -0
  39. src/index.tsx +20 -0
  40. src/logo.svg +1 -0
  41. src/picture/linkedin-icon.png +3 -0
  42. src/picture/logo.png +3 -0
  43. src/react-app-env.d.ts +1 -0
  44. src/reportWebVitals.ts +15 -0
  45. src/setupTests.ts +5 -0
  46. tsconfig.json +26 -0
  47. vector_db_chroma/chroma.sqlite3 +3 -0
  48. vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/data_level0.bin +3 -0
  49. vector_db_chroma/e00074a2-0e3e-4a43-a595-44f28c720a1a/header.bin +3 -0
  50. 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

  • SHA256: c386396ec70db3608075b5fbfaac4ab1ccaa86ba05a68ab393ec551eb66c3e00
  • Pointer size: 129 Bytes
  • Size of remote file: 5.35 kB
build/logo512.png ADDED

Git LFS Details

  • SHA256: 9ea4f4da7050c0cc408926f6a39c253624e9babb1d43c7977cd821445a60b461
  • Pointer size: 129 Bytes
  • Size of remote file: 9.66 kB
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

  • SHA256: c386396ec70db3608075b5fbfaac4ab1ccaa86ba05a68ab393ec551eb66c3e00
  • Pointer size: 129 Bytes
  • Size of remote file: 5.35 kB
public/logo512.png ADDED

Git LFS Details

  • SHA256: 9ea4f4da7050c0cc408926f6a39c253624e9babb1d43c7977cd821445a60b461
  • Pointer size: 129 Bytes
  • Size of remote file: 9.66 kB
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

  • SHA256: bb9a6e75e894c622ccaaf2c96edb0b0b5ded447d68c38782d9da2473d59b3638
  • Pointer size: 131 Bytes
  • Size of remote file: 126 kB
src/picture/logo.png ADDED

Git LFS Details

  • SHA256: 00c69b4bc32fa53885382e81292965c3a5a432b4d92221d398c87a805c435db4
  • Pointer size: 130 Bytes
  • Size of remote file: 50.9 kB
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