diff --git a/usecase/document-based-application/.gitignore b/usecase/document-based-application/.gitignore
new file mode 100644
index 0000000..d4b8db5
--- /dev/null
+++ b/usecase/document-based-application/.gitignore
@@ -0,0 +1,31 @@
+# General
+.env
+
+# Node.js
+/node_modules
+/frontend/node_modules
+
+# Next.js
+/.next/
+/out/
+/frontend/.next
+
+# Production Build
+/build
+
+# Debug Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# TypeScript
+*.tsbuildinfo
+next-env.d.ts
+
+# Vercel
+.vercel
+
+# Miscellaneous
+*.pyc
+
diff --git a/usecase/document-based-application/README.md b/usecase/document-based-application/README.md
index 82c795f..f232c8b 100644
--- a/usecase/document-based-application/README.md
+++ b/usecase/document-based-application/README.md
@@ -1,20 +1,33 @@
-> ๐ข **Notice:**
-> All teams submitting their project must create a `README.md` file following this guideline.
-> Please make sure to replace all placeholder texts (e.g., [Project Title], [Describe feature]) with actual content.
-
-# ๐ ๏ธ [Project Title]
+# ๐ DocZilla
### ๐ Overview
-This project was developed as part of the Document Based Application Hackathon. It aims to solve [describe the problem or goal].
+This project was developed as part of the AGI Agent Application Hackathon. It aims to solve the challenge of understanding, verifying, and summarizing documents (such as contracts or policies) using an LLM-powered agent, enabling automated compliance checks, summarization, and intelligent extraction of insights.
### ๐ Key Features
-- โ
**Feature 1**: [Describe feature]
-- โ
**Feature 2**: [Describe feature]
-- โ
**Feature 3**: [Describe feature]
+- โ
**Document Upload & Parsing**: Admins and users can upload documents which are parsed, summarized, or checked against internal policies.
+- โ
**Slack Notification**: Automatically generates a summary of the latest uploaded document and sends it to a Slack channel via webhook.
+- โ
**Compliance Checklist Extraction**: Parses internal policy documents and uses the LLM to extract a structured, testable checklist to compare against future contracts.
+### Demo ๐
+Experience the project in action with our live demo. Click the link below for a guided walkthrough showcasing document upload, parsing, compliance checklist extraction, and Slack integration.
+
+[Watch Live Demo](https://example.com/demo)
+
+
+
+
+
+
+
+[Watch Demo Video](https://drive.google.com/file/d/16xkGJr7-6aurjEV-tFU9HOB-LRJxrJI7/view?usp=sharing)
+
+### ๐งฉ Tech Stack
+- **Frontend**: Next.js, Tailwind CSS
+- **Backend**: FastAPI
+- **Database**: FAISS (vector database)
+- **Others**: OpenAI API (solar-pro via Upstage), LangChain, Slack Webhooks
+
+### ๐๏ธ Project Structure
-### ๐ผ๏ธ Demo / Screenshots
-
-[Optional demo video link: e.g., YouTube]
### ๐งฉ Tech Stack
- **Frontend**: [e.g., React, Vue, HTML/CSS]
@@ -24,19 +37,81 @@ This project was developed as part of the Document Based Application Hackathon.
### ๐๏ธ Project Structure
```
-๐ project-name/
+๐ The-Nomads/
โโโ frontend/
+โ โโโ components.json # Central config for UI component references.
+โ โโโ next.config.mjs # Next.js custom configuration settings.
+โ โโโ package-lock.json # Auto-generated lockfile for npm dependency versions.
+โ โโโ package.json # Project metadata and frontend dependencies.
+โ โโโ tailwind.config.ts # Tailwind CSS theme and utility configuration.
+โ โโโ tsconfig.json # TypeScript compiler options and path aliases.
+โ โโโ app/
+โ โ โโโ globals.css # Global styles applied across the entire app.
+โ โ โโโ layout copy.tsx # Backup or experimental layout file.
+โ โ โโโ layout.tsx # Root layout component for consistent app structure.
+โ โ โโโ page.tsx # Landing page of the application.
+โ โ โโโ dashboard/
+โ โ โ โโโ page.tsx # User dashboard displaying chat interface and uploads.
+โ โ โโโ pricing/
+โ โ โโโ page.tsx # Pricing page showing subscription plans.
+โ โโโ components/
+โ โ โโโ action-panel.tsx # Button panel for user interactions.
+โ โ โโโ action-sidebar.tsx # Sidebar showing actions or document options.
+โ โ โโโ app-sidebar.tsx # Main app sidebar for navigation.
+โ โ โโโ chat-interface.tsx # Main component for displaying chat sessions.
+โ โ โโโ chat-panel.tsx # Panel to display chat messages and input field.
+โ โ โโโ file-uploader.tsx # Component for uploading policy and contract files.
+โ โ โโโ landing-page.tsx # Component structuring the homepage content.
+โ โ โโโ login-form.tsx # Authentication form for login.
+โ โ โโโ signup-form.tsx # Authentication form for new user registration.
+โ โ โโโ theme-provider.tsx # Context for dark/light mode and theme settings.
+โ โ โโโ pricing/
+โ โ โ โโโ pricing-cards.tsx # Reusable pricing plan cards.
+โ โ โ โโโ pricing-header.tsx # Header text and layout for pricing section.
+โ โ โ โโโ pricing-toggle.tsx # Toggle between monthly/yearly billing.
+โ โ โโโ ui/ # Shared low-level UI components (e.g., buttons, modals).
+โ โโโ hooks/ # Custom React hooks for managing state and logic.
+โ โโโ lib/ # Helper functions and client-side utilities.
+โ โโโ public/ # Static files like logos, icons, etc.
+โ โโโ services/
+โ โ โโโ chat.ts # API handler for sending/receiving chat messages.
+โ โ โโโ upload.ts # API handler for file upload requests.
+โ โโโ styles/
+โ โ โโโ globals.css # Duplicate reference for styling, used by legacy files.
+โ โโโ types/
+โ โโโ chat.ts # Type definitions related to chat message objects.
โโโ backend/
-โโโ assets/
-โโโ README.md
-โโโ ...
+โ โโโ main.py # FastAPI app
+โ โโโ routes/
+โ โ โโโ upload.py # uploading documents to be parsed and then stored in the vector database
+โ โ โโโ query.py # embedding the query (prompt), and compare it to the vectore database using cosine similarity
+โ โ โโโ chat.py # managing the session connection between the user and the LLM
+โ โ โโโ slack.py # summarizing the document and send the summary to slack
+โ โ โโโ policy.py # uploading the internal policy to the database and checking the contracts against it
+โ โโโ utils/
+โ โ โโโ parse.py # Upstage API helper for parsing docs
+โ โ โโโ ocr.py # Upstage API helper for ocr images
+โ โ โโโ embedding.py # converting string to vector representations (embeddings)
+โ โ โโโ chunk.py # dividing the big documents into smaller, relatable parts, utilizing metadata
+โ โ โโโ llm.py # managing chat sessions while keeping the history
+โ โ โโโ retrieve.py # comparing the queries to the vectors in the database and retrieve the most similar ones
+โ โโโ vector_store/
+โ โ โโโ embedding_store.pkl # saved FAISS index
+โ โ โโโ latest_document.pkl # keeping track of the latest added document (is overwritten every time we upload)
+โ โโโ schemas/
+โ โ โโโ chat_payload.py # pydantic model for the chat session object
+โ โโโ data/
+โ โ โโโ policy_checklist.txt # the extracted checklist from the policy
+โ โ โโโ latest_document.txt # the parsed data from the latest document
+โ โโโ .env # UPSTAGE_API_KEY, etc.
+โโโ README.md
```
### ๐ง Setup & Installation
```bash
# Clone the repository
-git clone https://github.com/UpstageAI/cookbook/usecase/agi-agent-application/[repo-name].git
+git clone https://github.com/M-Abdelhakem/the-nomads.git
# Move to the frontend directory and run
cd frontend
@@ -46,28 +121,29 @@ npm run dev
# Move to the backend directory and run
cd backend
pip install -r requirements.txt
-python app.py
+uvicorn main:app --reload
```
### ๐ Dataset & References
-- **Dataset used**: [source and brief explanation]
+- **Dataset used**: We generated mock data to simulate some of the usecases we tackled
- **References / Resources**:
- [link 1]
- [link 2]
+[Internal Policy](https://docs.google.com/document/d/1XjfkSbxQ71sWHhwZoj0pSUSvL26ohWWMSRlrKMtejeE/edit?usp=sharing)
+[Contract 1](https://docs.google.com/document/d/15c5c4Q74BrJIYqFnrnuER_DL_DUXL1kRM2-6qZOUvFk/edit?usp=sharing)
+[Contract 2](https://docs.google.com/document/d/1YufhygMcE5dVumpEL7naMiaBGG6DSTW3HV1vMPWIKKs/edit?usp=sharing)
### ๐ Team Members
| Name | Role | GitHub |
|-------------|--------------------|------------------------------------|
-| Kim Ups | Frontend Developer | [@kimups](https://github.com/johndoe) |
-| Park Stage | Backend Developer | [@parkstage](https://github.com/janedev) |
+| Mohanad Abdelhakem | Full-Stack Developer | [@M-Abdelhakem](https://github.com/M-Abdelhakem) |
+| Madiyar Zhunussov | Full-Stack Developer | [@madiyarzm](https://github.com/madiyarzm) |
### โฐ Development Period
-- Last updated: YYYY-MM-DD
+- Last updated: 2025-04-13
### ๐ License
This project is licensed under the [MIT license](https://opensource.org/licenses/MIT).
See the LICENSE file for more details.
### ๐ฌ Additional Notes
-- Feel free to include any other relevant notes or links here.
+- Feel free to include any other relevant notes or links here.
\ No newline at end of file
diff --git a/usecase/document-based-application/backend/data/company_policy.txt b/usecase/document-based-application/backend/data/company_policy.txt
new file mode 100644
index 0000000..8afd39f
--- /dev/null
+++ b/usecase/document-based-application/backend/data/company_policy.txt
@@ -0,0 +1 @@
+{'api': '2.0', 'content': {'html': "
INTERNAL POLICY
\nEffective Date: March 1, 2025 Last reviewed by Legal: March 1, 2025 All contracts executed between NorthBeam Solutions and third-party vendors must adhere to these guidelines:
\n1. Payment Terms \nยท Initial upfront payment must not exceed 30% of the total contract value. ยท Remaining payments must be tied explicitly to deliverables or milestones.
\n2. Contract Duration and Renewal
\nยท Maximum initial contract duration: 24 months. โ No auto-renewals; explicit written renewal required at least 90 days prior to contract expiration.
\n3. Liability & Insurance \nVendor must maintain a liability insurance policy of at least $1,000,000 USD coverage for the duration of the contract.
\n4. Confidentiality & Data Security \nยท Contract must include clauses prohibiting reverse engineering and unauthorized disclosure of confidential information. โ Vendor must comply with NorthBeam Corporation's data protection standards (ISO 27001 compliant).
\n5. Dispute Resolution \nContracts must specify mediation as the first step of dispute resolution. Arbitration, if needed, must take place in New York State under American Arbitration Association (AAA) rules.
\nInternal Policy
\n ", 'markdown': '', 'text': ''}, 'elements': [{'category': 'paragraph', 'content': {'html': "INTERNAL POLICY
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.4036, 'y': 0.0915}, {'x': 0.5977, 'y': 0.0915}, {'x': 0.5977, 'y': 0.1097}, {'x': 0.4036, 'y': 0.1097}], 'id': 0, 'page': 1}, {'category': 'paragraph', 'content': {'html': "Effective Date: March 1, 2025 Last reviewed by Legal: March 1, 2025 All contracts executed between NorthBeam Solutions and third-party vendors must adhere to these guidelines:
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1128, 'y': 0.1454}, {'x': 0.8473, 'y': 0.1454}, {'x': 0.8473, 'y': 0.2214}, {'x': 0.1128, 'y': 0.2214}], 'id': 1, 'page': 1}, {'category': 'heading1', 'content': {'html': "1. Payment Terms ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1131, 'y': 0.2295}, {'x': 0.2793, 'y': 0.2295}, {'x': 0.2793, 'y': 0.248}, {'x': 0.1131, 'y': 0.248}], 'id': 2, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Initial upfront payment must not exceed 30% of the total contract value. ยท Remaining payments must be tied explicitly to deliverables or milestones.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1425, 'y': 0.2702}, {'x': 0.8208, 'y': 0.2702}, {'x': 0.8208, 'y': 0.3193}, {'x': 0.1425, 'y': 0.3193}], 'id': 3, 'page': 1}, {'category': 'paragraph', 'content': {'html': "2. Contract Duration and Renewal
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1139, 'y': 0.344}, {'x': 0.4271, 'y': 0.344}, {'x': 0.4271, 'y': 0.3637}, {'x': 0.1139, 'y': 0.3637}], 'id': 4, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Maximum initial contract duration: 24 months. โ No auto-renewals; explicit written renewal required at least 90 days prior to contract expiration.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1417, 'y': 0.3855}, {'x': 0.8397, 'y': 0.3855}, {'x': 0.8397, 'y': 0.4618}, {'x': 0.1417, 'y': 0.4618}], 'id': 5, 'page': 1}, {'category': 'heading1', 'content': {'html': "3. Liability & Insurance ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.113, 'y': 0.4855}, {'x': 0.3327, 'y': 0.4855}, {'x': 0.3327, 'y': 0.5053}, {'x': 0.113, 'y': 0.5053}], 'id': 6, 'page': 1}, {'category': 'paragraph', 'content': {'html': "Vendor must maintain a liability insurance policy of at least $1,000,000 USD coverage for the duration of the contract.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1422, 'y': 0.5277}, {'x': 0.8385, 'y': 0.5277}, {'x': 0.8385, 'y': 0.5762}, {'x': 0.1422, 'y': 0.5762}], 'id': 7, 'page': 1}, {'category': 'heading1', 'content': {'html': "4. Confidentiality & Data Security ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1132, 'y': 0.6006}, {'x': 0.4255, 'y': 0.6006}, {'x': 0.4255, 'y': 0.6197}, {'x': 0.1132, 'y': 0.6197}], 'id': 8, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Contract must include clauses prohibiting reverse engineering and unauthorized disclosure of confidential information. โ Vendor must comply with NorthBeam Corporation's data protection standards (ISO 27001 compliant).
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1436, 'y': 0.6417}, {'x': 0.8809, 'y': 0.6417}, {'x': 0.8809, 'y': 0.7454}, {'x': 0.1436, 'y': 0.7454}], 'id': 9, 'page': 1}, {'category': 'heading1', 'content': {'html': "5. Dispute Resolution ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1138, 'y': 0.7702}, {'x': 0.316, 'y': 0.7702}, {'x': 0.316, 'y': 0.7893}, {'x': 0.1138, 'y': 0.7893}], 'id': 10, 'page': 1}, {'category': 'list', 'content': {'html': "Contracts must specify mediation as the first step of dispute resolution. Arbitration, if needed, must take place in New York State under American Arbitration Association (AAA) rules.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1424, 'y': 0.8115}, {'x': 0.8275, 'y': 0.8115}, {'x': 0.8275, 'y': 0.8876}, {'x': 0.1424, 'y': 0.8876}], 'id': 11, 'page': 1}, {'category': 'paragraph', 'content': {'html': "Internal Policy
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1145, 'y': 0.9157}, {'x': 0.2138, 'y': 0.9157}, {'x': 0.2138, 'y': 0.9305}, {'x': 0.1145, 'y': 0.9305}], 'id': 12, 'page': 1}, {'category': 'footer', 'content': {'html': " ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.8717, 'y': 0.9322}, {'x': 0.8862, 'y': 0.9322}, {'x': 0.8862, 'y': 0.9459}, {'x': 0.8717, 'y': 0.9459}], 'id': 13, 'page': 1}], 'merged_elements': [], 'model': 'document-parse-250116', 'ocr': True, 'usage': {'pages': 1}}
\ No newline at end of file
diff --git a/usecase/document-based-application/backend/data/latest_document.txt b/usecase/document-based-application/backend/data/latest_document.txt
new file mode 100644
index 0000000..703ba92
--- /dev/null
+++ b/usecase/document-based-application/backend/data/latest_document.txt
@@ -0,0 +1 @@
+{'api': '2.0', 'content': {'html': "NorthBeam Corporation - Vendor Agreement Effective Date: April 13, 2025 Vendor: Acme Analytics Inc. Client: NorthBeam Corporation
\n1. Scope of Services \nAcme Analytics Inc. shall provide strategic market research and business consulting services for NorthBeam Corporation's upcoming product launch.
\n2. Payment Terms \nยท Total Contract Value: $100,000 USD
\nยท Initial upfront payment: $30,000 USD (30% of total value)
\nยท Remaining payments:
\nโ $35,000 upon delivery of Phase 1 Report
\nโ $35,000 upon delivery of Final Strategic Recommendations
\n3. Contract Duration and Renewal \nยท Initial term: 18 months
\nยท This agreement will not auto-renew.
\nยท Renewal requires explicit written consent from both parties, submitted at least 90 days prior to expiration.
\n4. Liability & Insurance \nVendor shall maintain liability insurance coverage of at least $1,000,000 USD throughout the contract duration. Proof of insurance must be submitted within 10 business days of the effective date.
\n5. Confidentiality & Data Security
\nVendor agrees to: \nNot reverse engineer or attempt to derive source code from any proprietary systems or tools.
\nยท Not disclose confidential information to third parties without written permission.
\nComply fully with NorthBeam Corporation's data protection standards, including ISO 27001 requirements.
\n6. Dispute Resolution \nยท Parties agree to attempt mediation as the first step in resolving any disputes.
\nIf unresolved, disputes will be settled by binding arbitration in New York State under American Arbitration Association (AAA) rules.
", 'markdown': '', 'text': ''}, 'elements': [{'category': 'paragraph', 'content': {'html': "NorthBeam Corporation - Vendor Agreement Effective Date: April 13, 2025 Vendor: Acme Analytics Inc. Client: NorthBeam Corporation
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1166, 'y': 0.1055}, {'x': 0.509, 'y': 0.1055}, {'x': 0.509, 'y': 0.1781}, {'x': 0.1166, 'y': 0.1781}], 'id': 0, 'page': 1}, {'category': 'heading1', 'content': {'html': "1. Scope of Services ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1161, 'y': 0.1957}, {'x': 0.3278, 'y': 0.1957}, {'x': 0.3278, 'y': 0.2151}, {'x': 0.1161, 'y': 0.2151}], 'id': 1, 'page': 1}, {'category': 'paragraph', 'content': {'html': "Acme Analytics Inc. shall provide strategic market research and business consulting services for NorthBeam Corporation's upcoming product launch.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1138, 'y': 0.2311}, {'x': 0.8854, 'y': 0.2311}, {'x': 0.8854, 'y': 0.2678}, {'x': 0.1138, 'y': 0.2678}], 'id': 2, 'page': 1}, {'category': 'heading1', 'content': {'html': "2. Payment Terms ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1138, 'y': 0.2837}, {'x': 0.3008, 'y': 0.2837}, {'x': 0.3008, 'y': 0.304}, {'x': 0.1138, 'y': 0.304}], 'id': 3, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Total Contract Value: $100,000 USD
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1449, 'y': 0.3208}, {'x': 0.4674, 'y': 0.3208}, {'x': 0.4674, 'y': 0.3371}, {'x': 0.1449, 'y': 0.3371}], 'id': 4, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Initial upfront payment: $30,000 USD (30% of total value)
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1432, 'y': 0.3577}, {'x': 0.6328, 'y': 0.3577}, {'x': 0.6328, 'y': 0.3757}, {'x': 0.1432, 'y': 0.3757}], 'id': 5, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Remaining payments:
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1414, 'y': 0.3937}, {'x': 0.3537, 'y': 0.3937}, {'x': 0.3537, 'y': 0.4117}, {'x': 0.1414, 'y': 0.4117}], 'id': 6, 'page': 1}, {'category': 'list', 'content': {'html': "โ $35,000 upon delivery of Phase 1 Report
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.2019, 'y': 0.4306}, {'x': 0.567, 'y': 0.4306}, {'x': 0.567, 'y': 0.4485}, {'x': 0.2019, 'y': 0.4485}], 'id': 7, 'page': 1}, {'category': 'list', 'content': {'html': "โ $35,000 upon delivery of Final Strategic Recommendations
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.2015, 'y': 0.467}, {'x': 0.7099, 'y': 0.467}, {'x': 0.7099, 'y': 0.4848}, {'x': 0.2015, 'y': 0.4848}], 'id': 8, 'page': 1}, {'category': 'heading1', 'content': {'html': "3. Contract Duration and Renewal ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.115, 'y': 0.5199}, {'x': 0.4606, 'y': 0.5199}, {'x': 0.4606, 'y': 0.5383}, {'x': 0.115, 'y': 0.5383}], 'id': 9, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Initial term: 18 months
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1427, 'y': 0.5561}, {'x': 0.3566, 'y': 0.5561}, {'x': 0.3566, 'y': 0.5728}, {'x': 0.1427, 'y': 0.5728}], 'id': 10, 'page': 1}, {'category': 'list', 'content': {'html': "ยท This agreement will not auto-renew.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1428, 'y': 0.5934}, {'x': 0.4715, 'y': 0.5934}, {'x': 0.4715, 'y': 0.6103}, {'x': 0.1428, 'y': 0.6103}], 'id': 11, 'page': 1}, {'category': 'list', 'content': {'html': "ยท Renewal requires explicit written consent from both parties, submitted at least 90 days prior to expiration.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1437, 'y': 0.6296}, {'x': 0.8872, 'y': 0.6296}, {'x': 0.8872, 'y': 0.6647}, {'x': 0.1437, 'y': 0.6647}], 'id': 12, 'page': 1}, {'category': 'heading1', 'content': {'html': "4. Liability & Insurance ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1147, 'y': 0.7002}, {'x': 0.3521, 'y': 0.7002}, {'x': 0.3521, 'y': 0.7201}, {'x': 0.1147, 'y': 0.7201}], 'id': 13, 'page': 1}, {'category': 'paragraph', 'content': {'html': "Vendor shall maintain liability insurance coverage of at least $1,000,000 USD throughout the contract duration. Proof of insurance must be submitted within 10 business days of the effective date.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1134, 'y': 0.7366}, {'x': 0.88, 'y': 0.7366}, {'x': 0.88, 'y': 0.79}, {'x': 0.1134, 'y': 0.79}], 'id': 14, 'page': 1}, {'category': 'paragraph', 'content': {'html': "5. Confidentiality & Data Security
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1144, 'y': 0.8073}, {'x': 0.4546, 'y': 0.8073}, {'x': 0.4546, 'y': 0.8285}, {'x': 0.1144, 'y': 0.8285}], 'id': 15, 'page': 1}, {'category': 'heading1', 'content': {'html': "Vendor agrees to: ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1138, 'y': 0.8437}, {'x': 0.262, 'y': 0.8437}, {'x': 0.262, 'y': 0.8614}, {'x': 0.1138, 'y': 0.8614}], 'id': 16, 'page': 1}, {'category': 'list', 'content': {'html': "Not reverse engineer or attempt to derive source code from any proprietary systems or tools.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1413, 'y': 0.0907}, {'x': 0.8701, 'y': 0.0907}, {'x': 0.8701, 'y': 0.1264}, {'x': 0.1413, 'y': 0.1264}], 'id': 17, 'page': 2}, {'category': 'list', 'content': {'html': "ยท Not disclose confidential information to third parties without written permission.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.144, 'y': 0.1456}, {'x': 0.8018, 'y': 0.1456}, {'x': 0.8018, 'y': 0.1641}, {'x': 0.144, 'y': 0.1641}], 'id': 18, 'page': 2}, {'category': 'list', 'content': {'html': "Comply fully with NorthBeam Corporation's data protection standards, including ISO 27001 requirements.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1438, 'y': 0.1826}, {'x': 0.8469, 'y': 0.1826}, {'x': 0.8469, 'y': 0.2183}, {'x': 0.1438, 'y': 0.2183}], 'id': 19, 'page': 2}, {'category': 'heading1', 'content': {'html': "6. Dispute Resolution ", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1153, 'y': 0.2538}, {'x': 0.3378, 'y': 0.2538}, {'x': 0.3378, 'y': 0.2733}, {'x': 0.1153, 'y': 0.2733}], 'id': 20, 'page': 2}, {'category': 'list', 'content': {'html': "ยท Parties agree to attempt mediation as the first step in resolving any disputes.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1428, 'y': 0.2898}, {'x': 0.8079, 'y': 0.2898}, {'x': 0.8079, 'y': 0.3079}, {'x': 0.1428, 'y': 0.3079}], 'id': 21, 'page': 2}, {'category': 'list', 'content': {'html': "If unresolved, disputes will be settled by binding arbitration in New York State under American Arbitration Association (AAA) rules.
", 'markdown': '', 'text': ''}, 'coordinates': [{'x': 0.1428, 'y': 0.3259}, {'x': 0.8663, 'y': 0.3259}, {'x': 0.8663, 'y': 0.3633}, {'x': 0.1428, 'y': 0.3633}], 'id': 22, 'page': 2}], 'merged_elements': [], 'model': 'document-parse-250116', 'ocr': True, 'usage': {'pages': 2}}
\ No newline at end of file
diff --git a/usecase/document-based-application/backend/data/policy_checklist.txt b/usecase/document-based-application/backend/data/policy_checklist.txt
new file mode 100644
index 0000000..b7c7f97
--- /dev/null
+++ b/usecase/document-based-application/backend/data/policy_checklist.txt
@@ -0,0 +1,9 @@
+- Initial upfront payment must not exceed 30% of the total contract value
+- Remaining payments must be tied explicitly to deliverables or milestones
+- Maximum initial contract duration: 24 months
+- No auto-renewals; explicit written renewal required at least 90 days prior to contract expiration
+- Vendor must maintain a liability insurance policy of at least $1,000,000 USD coverage for the duration of the contract
+- Contract must include clauses prohibiting reverse engineering and unauthorized disclosure of confidential information
+- Vendor must comply with NorthBeam Corporation's data protection standards (ISO 27001 compliant)
+- Contracts must specify mediation as the first step of dispute resolution
+- Arbitration, if needed, must take place in New York State under American Arbitration Association (AAA) rules
diff --git a/usecase/document-based-application/backend/main.py b/usecase/document-based-application/backend/main.py
new file mode 100644
index 0000000..e285d4c
--- /dev/null
+++ b/usecase/document-based-application/backend/main.py
@@ -0,0 +1,35 @@
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from routes.upload import upload_router
+from routes.query import query_router
+from routes.chat import chat_router
+from routes.slack import slack_router
+from routes.policy import policy_router
+
+app = FastAPI(title="Echo โ Contract Assistant")
+
+# Register routers
+app.include_router(upload_router)
+app.include_router(query_router)
+app.include_router(chat_router)
+app.include_router(slack_router)
+app.include_router(policy_router)
+# Allowed frontend URLs
+origins = [
+ "http://localhost:3000", # Local dev
+ # "https://www.echodoc.app", # Production
+]
+
+# CORS Middleware
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/usecase/document-based-application/backend/requirements.txt b/usecase/document-based-application/backend/requirements.txt
new file mode 100644
index 0000000..92b427e
--- /dev/null
+++ b/usecase/document-based-application/backend/requirements.txt
@@ -0,0 +1,6 @@
+fastapi==0.110.0
+uvicorn==0.29.0
+openai==1.52.2
+langchain==0.3.23
+python-dotenv==1.0.1
+pydantic==2.6.4
\ No newline at end of file
diff --git a/usecase/document-based-application/backend/routes/chat.py b/usecase/document-based-application/backend/routes/chat.py
new file mode 100644
index 0000000..d1a33f1
--- /dev/null
+++ b/usecase/document-based-application/backend/routes/chat.py
@@ -0,0 +1,90 @@
+from fastapi import APIRouter, HTTPException
+from schemas.chat_payload import ChatPayload, Message
+from utils.embedding import embed_text
+from utils.retrieve import retrieve_similar_chunks
+from langchain_core.documents import Document
+from openai import OpenAI
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+chat_router = APIRouter()
+
+UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")
+if not UPSTAGE_API_KEY:
+ raise ValueError("Missing UPSTAGE_API_KEY")
+
+client = OpenAI(
+ api_key=UPSTAGE_API_KEY,
+ base_url="https://api.upstage.ai/v1"
+)
+
+# System prompt template
+SYSTEM_TEMPLATE = """You are a helpful assistant that answers questions based on the provided document excerpts.
+Here are relevant excerpts:
+{chunks}
+
+Respond clearly and concisely based only on the given information unless instructed otherwise."""
+
+@chat_router.post("/chat", tags=["Chat"])
+async def chat_with_upstage(payload: ChatPayload):
+ try:
+ messages = payload.messages
+
+ # Use the last user message to generate the query embedding
+ last_user_message = next((m.content for m in reversed(messages) if m.role == "user"), None)
+
+ if not last_user_message:
+ raise HTTPException(status_code=400, detail="No user message found in history.")
+
+ # Initialize context chunks
+ context_chunks = []
+
+ # Add document content if available
+ if payload.documentContent:
+ try:
+ # Split document content into chunks if it's too long
+ doc_chunks = [payload.documentContent[i:i+1000] for i in range(0, len(payload.documentContent), 1000)]
+ context_chunks.extend(doc_chunks)
+ except Exception as e:
+ print(f"Error processing document content: {str(e)}")
+ # Continue without document content if there's an error
+
+ # Generate query embedding
+ query_doc = Document(page_content=last_user_message)
+ query_embedding = embed_text([query_doc], is_query=True)
+
+ # Retrieve similar chunks
+ try:
+ similar_chunks = retrieve_similar_chunks(query_embedding)
+ context_chunks.extend(similar_chunks)
+ except Exception as e:
+ print(f"Error retrieving similar chunks: {str(e)}")
+ # Continue with just document content if retrieval fails
+
+ # Format system message
+ system_message = SYSTEM_TEMPLATE.format(chunks="\n\n".join(f"- {chunk}" for chunk in context_chunks))
+
+ # Prepare conversation
+ full_convo = [Message(role="system", content=system_message)] + messages
+
+ # Call the model
+ try:
+ response = client.chat.completions.create(
+ model="solar-pro",
+ messages=[msg.dict() for msg in full_convo]
+ )
+
+ assistant_reply = response.choices[0].message.content
+ updated_history = messages + [Message(role="assistant", content=assistant_reply)]
+
+ return {"messages": updated_history}
+ except Exception as e:
+ print(f"Error calling LLM: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Error generating response: {str(e)}")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ print(f"Unexpected error in chat_with_upstage: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
diff --git a/usecase/document-based-application/backend/routes/policy.py b/usecase/document-based-application/backend/routes/policy.py
new file mode 100644
index 0000000..471ba22
--- /dev/null
+++ b/usecase/document-based-application/backend/routes/policy.py
@@ -0,0 +1,149 @@
+from fastapi import APIRouter, UploadFile, File, HTTPException
+from utils.llm import chat_with_solar, chat_with_context
+from utils.parse import parse_pdf
+from utils.embedding import embed_text
+from utils.retrieve import retrieve_similar_chunks
+from langchain_core.documents import Document
+import os
+import tempfile
+import pickle
+
+policy_router = APIRouter()
+
+@policy_router.post("/upload-policy", tags=["Policy"])
+async def upload_company_policy(file: UploadFile = File(...)):
+ if not file.filename.endswith(".pdf"):
+ raise HTTPException(status_code=400, detail="Only PDF files are supported for policy.")
+
+ # Create a temporary file to store the uploaded PDF
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
+ content = await file.read()
+ temp_file.write(content)
+ temp_file_path = temp_file.name
+
+ try:
+ # Parse the PDF to extract text
+ policy_text = parse_pdf(temp_file_path)
+
+ # Save the extracted text to a file for reference
+ policy_path = "data/company_policy.txt"
+ with open(policy_path, "w", encoding="utf-8") as f:
+ f.write(policy_text)
+
+ # LLM Prompt
+ prompt = (
+ "Based on the internal policy document below, create a list of specific, testable conditions "
+ "that a contract must meet to be compliant. Format your response ONLY as a bullet-point list "
+ "starting each line with '- '. Do not include any other text or explanations.\n\n"
+ f"{policy_text}"
+ )
+
+ # Get the checklist from the LLM
+ checklist_text = chat_with_solar(prompt)
+
+ # Process the checklist to ensure it's a proper list
+ # Split by newlines and filter out empty lines
+ checklist_items = [item.strip() for item in checklist_text.split('\n') if item.strip()]
+
+ # Remove bullet points or numbering if present
+ checklist_items = [item.lstrip('โข-*0123456789. ') for item in checklist_items]
+
+ # Save the checklist to a file
+ checklist_path = "data/policy_checklist.txt"
+ with open(checklist_path, "w", encoding="utf-8") as f:
+ for item in checklist_items:
+ f.write(f"- {item}\n")
+
+ return {
+ "status": "success",
+ "checklist": checklist_items,
+ "message": f"Checklist saved to {checklist_path}"
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to process policy: {str(e)}")
+ finally:
+ # Clean up the temporary file
+ if os.path.exists(temp_file_path):
+ os.unlink(temp_file_path)
+
+@policy_router.post("/compare-policy", tags=["Policy"])
+async def compare_policy():
+ """
+ Compare the latest document against the internal policy checklist.
+ This endpoint will:
+ 1. Read the latest document from /data/latest_document.txt
+ 2. Embed it and store in a separate pickle file
+ 3. Read the policy checklist from /data/policy_checklist.txt
+ 4. For each checklist item, use RAG to check if it's satisfied in the document
+ 5. Return a formatted checklist with checks/crosses for satisfied/unsatisfied conditions
+ """
+ try:
+ # Check if the latest document exists
+ latest_doc_path = "data/latest_document.txt"
+ if not os.path.exists(latest_doc_path):
+ raise HTTPException(status_code=404, detail="Latest document not found. Please upload a document first.")
+
+ # Check if the policy checklist exists
+ checklist_path = "data/policy_checklist.txt"
+ if not os.path.exists(checklist_path):
+ raise HTTPException(status_code=404, detail="Policy checklist not found. Please upload a policy first.")
+
+ # Read the latest document
+ with open(latest_doc_path, "r", encoding="utf-8") as f:
+ document_text = f.read()
+
+ # Read the policy checklist
+ with open(checklist_path, "r", encoding="utf-8") as f:
+ checklist_lines = f.readlines()
+
+ # Process checklist items (remove bullet points and empty lines)
+ checklist_items = [line.strip().lstrip('- ') for line in checklist_lines if line.strip()]
+
+ # Embed the document and store in a separate pickle file
+ doc_chunks = [document_text[i:i+1000] for i in range(0, len(document_text), 1000)]
+ doc_documents = [Document(page_content=chunk) for chunk in doc_chunks]
+ embed_text(doc_documents, embedding_store_path="vector_store/latest_document.pkl")
+
+ # For each checklist item, check if it's satisfied in the document
+ results = []
+ for item in checklist_items:
+ # Create a query from the checklist item
+ query = f"Does the document satisfy this condition: {item}? Answer with 'Yes' or 'No' and explain why."
+
+ # Get the query embedding
+ query_doc = Document(page_content=query)
+ query_embedding = embed_text([query_doc], is_query=True)
+
+ # Retrieve similar chunks from the document
+ similar_chunks = retrieve_similar_chunks(query_embedding, embedding_store_path="vector_store/latest_document.pkl")
+
+ # Use the LLM to determine if the condition is satisfied
+ response = chat_with_context(query, similar_chunks)
+
+ # Determine if the condition is satisfied based on the response
+ is_satisfied = "yes" in response.lower()[:10] # Simple heuristic, can be improved
+
+ # Add to results
+ results.append({
+ "condition": item,
+ "satisfied": is_satisfied,
+ "explanation": response
+ })
+
+ # Format the results for the chat
+ formatted_results = "Here's how the document compares to our internal policy:\n\n"
+ for result in results:
+ status = "โ
" if result["satisfied"] else "โ"
+ formatted_results += f"{status} {result['condition']}\n"
+ formatted_results += f" Explanation: {result['explanation']}\n\n"
+
+ return {
+ "status": "success",
+ "results": results,
+ "formatted_results": formatted_results
+ }
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to compare policy: {str(e)}")
diff --git a/usecase/document-based-application/backend/routes/query.py b/usecase/document-based-application/backend/routes/query.py
new file mode 100644
index 0000000..521a60b
--- /dev/null
+++ b/usecase/document-based-application/backend/routes/query.py
@@ -0,0 +1,25 @@
+from fastapi import APIRouter, HTTPException
+from langchain_core.documents import Document
+from utils.embedding import embed_text
+from utils.retrieve import retrieve_similar_chunks
+
+query_router = APIRouter()
+
+@query_router.post("/query", tags=["Query"])
+async def query_documents(payload: str):
+ try:
+ # Wrap the payload in a Document
+ query_doc = Document(page_content=payload)
+
+ # Embed the query
+ query_embeddings = embed_text([query_doc], is_query=True)
+
+ # Retrieve similar chunks from FAISS index
+ results = retrieve_similar_chunks(query_embeddings)
+
+ return {
+ "query": payload,
+ "relevant_chunks": results
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/usecase/document-based-application/backend/routes/slack.py b/usecase/document-based-application/backend/routes/slack.py
new file mode 100644
index 0000000..0ff565f
--- /dev/null
+++ b/usecase/document-based-application/backend/routes/slack.py
@@ -0,0 +1,83 @@
+from fastapi import APIRouter, HTTPException
+from openai import OpenAI
+import os
+from dotenv import load_dotenv
+import requests
+from typing import Optional
+
+slack_router = APIRouter()
+
+load_dotenv()
+
+# Initialize OpenAI client (Upstage Solar)
+client = OpenAI(
+ api_key=os.getenv("UPSTAGE_API_KEY"),
+ base_url="https://api.upstage.ai/v1"
+)
+
+# Slack webhook URL
+SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")
+
+# Path to latest document
+LATEST_DOC_PATH = "data/latest_document.txt"
+
+@slack_router.post("/send-slack", tags=["Slack"])
+async def send_slack_summary(only_summary: Optional[bool] = None):
+ try:
+ # Step 1: Read the latest document
+ if not os.path.exists(LATEST_DOC_PATH):
+ raise HTTPException(status_code=404, detail="Latest document not found.")
+
+ with open(LATEST_DOC_PATH, "r", encoding="utf-8") as f:
+ document_text = f.read()
+
+ if not document_text.strip():
+ raise HTTPException(status_code=400, detail="Latest document is empty.")
+
+ # Step 2: Ask the model for a summary
+ messages = [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant. Write an organized summary of the document in an organized and easy-to-follow way."
+ },
+ {
+ "role": "user",
+ "content": document_text
+ }
+ ]
+
+ response = client.chat.completions.create(
+ model="solar-pro",
+ messages=messages
+ )
+
+ summary = response.choices[0].message.content.strip()
+ if only_summary:
+ return {
+ "status": "success",
+ "message": "Summary sent to Slack successfully.",
+ "summary": summary
+ }
+
+ # Step 3: Send the summary to Slack
+ payload = {"text": f"*Summary of Latest Document:*\n{summary}"}
+ slack_response = requests.post(
+ SLACK_WEBHOOK_URL,
+ json=payload,
+ headers={"Content-Type": "application/json"}
+ )
+
+ if slack_response.status_code != 200:
+ raise HTTPException(
+ status_code=500,
+ detail=f"Failed to send message to Slack: {slack_response.text}"
+ )
+
+ return {
+ "status": "success",
+ "message": "Summary sent to Slack successfully.",
+ "summary": summary
+ }
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
diff --git a/usecase/document-based-application/backend/routes/upload.py b/usecase/document-based-application/backend/routes/upload.py
new file mode 100644
index 0000000..385a49e
--- /dev/null
+++ b/usecase/document-based-application/backend/routes/upload.py
@@ -0,0 +1,53 @@
+from fastapi import APIRouter, UploadFile, File, HTTPException
+from fastapi.responses import JSONResponse
+from utils.parse import parse_pdf
+from utils.ocr import ocr_image
+from utils.chunk import chunk_text
+from utils.embedding import embed_text
+import tempfile
+import shutil
+import os
+
+upload_router = APIRouter()
+
+ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/jpg"]
+ALLOWED_PDF_TYPE = "application/pdf"
+
+
+@upload_router.post("/upload", tags=["Upload"])
+async def upload_file(file: UploadFile = File(...)):
+ if file.content_type not in ALLOWED_IMAGE_TYPES + [ALLOWED_PDF_TYPE]:
+ raise HTTPException(status_code=400, detail="Unsupported file type. Only PDFs and images are allowed.")
+
+ try:
+ # Save to temporary file
+ suffix = os.path.splitext(file.filename)[-1]
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
+ shutil.copyfileobj(file.file, tmp)
+ tmp_path = tmp.name
+
+ # Use OCR for images, Parse for PDFs
+ if file.content_type in ALLOWED_IMAGE_TYPES:
+ extracted_text = ocr_image(tmp_path)
+ elif file.content_type == ALLOWED_PDF_TYPE:
+ extracted_text = parse_pdf(tmp_path)
+ else:
+ raise HTTPException(status_code=400, detail="Unsupported file type")
+
+ # Save the extracted text to a file
+ with open('data/latest_document.txt', 'w', encoding='utf-8') as f:
+ f.write(extracted_text)
+
+ # Chunking
+ chunks = chunk_text(extracted_text, file.filename)
+
+ # Embedding & storing
+ embed_text(chunks)
+
+ return JSONResponse(content={"message": "File processed, chunked, embedded and stored successfully!"})
+
+ finally:
+ try:
+ os.remove(tmp_path)
+ except Exception:
+ pass
diff --git a/usecase/document-based-application/backend/schemas/chat_payload.py b/usecase/document-based-application/backend/schemas/chat_payload.py
new file mode 100644
index 0000000..7f6ed74
--- /dev/null
+++ b/usecase/document-based-application/backend/schemas/chat_payload.py
@@ -0,0 +1,14 @@
+from pydantic import BaseModel, Field
+from typing import List, Literal, Optional
+
+
+class Message(BaseModel):
+ role: Literal["user", "assistant", "system"]
+ content: str
+ timestamp: Optional[str] = None
+
+
+class ChatPayload(BaseModel):
+ messages: List[Message] = Field(..., description="The message history including the user prompt")
+ documentId: Optional[str] = None
+ documentContent: Optional[str] = None
diff --git a/usecase/document-based-application/backend/utils/chunk.py b/usecase/document-based-application/backend/utils/chunk.py
new file mode 100644
index 0000000..3adda56
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/chunk.py
@@ -0,0 +1,36 @@
+from langchain.text_splitter import RecursiveCharacterTextSplitter
+from langchain_core.documents import Document
+from datetime import datetime
+
+def chunk_text(text: str, filename: str, chunk_size: int = 1000, chunk_overlap: int = 200) -> list[Document]:
+ """
+ Splits the input text into chunks with metadata.
+
+ Parameters:
+ - text: full text extracted from the uploaded file (HTML or plain)
+ - filename: name of the uploaded file (used in metadata)
+ - chunk_size: max tokens per chunk
+ - chunk_overlap: token overlap between chunks
+
+ Returns:
+ - list of langchain Document objects with metadata
+ """
+
+ splitter = RecursiveCharacterTextSplitter(
+ chunk_size=chunk_size,
+ chunk_overlap=chunk_overlap,
+ separators=["\n\n", "\n", " ", ""]
+ )
+
+ chunks = splitter.split_text(text)
+
+ docs = []
+ for i, chunk in enumerate(chunks):
+ metadata = {
+ "source": filename,
+ "chunk_index": i,
+ "upload_time": datetime.utcnow().isoformat()
+ }
+ docs.append(Document(page_content=chunk, metadata=metadata))
+
+ return docs
diff --git a/usecase/document-based-application/backend/utils/embedding.py b/usecase/document-based-application/backend/utils/embedding.py
new file mode 100644
index 0000000..1ad5ea0
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/embedding.py
@@ -0,0 +1,51 @@
+import pickle
+from langchain_upstage import UpstageEmbeddings
+from langchain_community.vectorstores import FAISS
+from langchain_core.documents import Document
+from typing import List
+import dotenv
+import os
+
+dotenv.load_dotenv()
+
+UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")
+if not UPSTAGE_API_KEY:
+ raise ValueError("UPSTAGE_API_KEY not found in environment variables.")
+
+# Models
+embedding_model = UpstageEmbeddings(
+ model="embedding-passage",
+ upstage_api_key=UPSTAGE_API_KEY
+)
+
+query_model = UpstageEmbeddings(
+ model="embedding-query",
+ upstage_api_key=UPSTAGE_API_KEY
+)
+
+def embed_text(chunks: List[Document], is_query: bool = False, embedding_store_path: str = "vector_store/embedding_store.pkl") -> List[List[float]]:
+ model = query_model if is_query else embedding_model
+
+ if is_query:
+ return model.embed_documents([doc.page_content for doc in chunks])[0]
+
+ # Embed and store passages
+ embeddings = model.embed_documents([doc.page_content for doc in chunks])
+ if os.path.exists(embedding_store_path):
+ with open(embedding_store_path, "rb") as f:
+ stored_data = pickle.load(f)
+ else:
+ stored_data = []
+ for embedding, doc in zip(embeddings, chunks):
+ stored_data.append({
+ "embedding": embedding,
+ "content": doc.page_content,
+ "metadata": doc.metadata
+ })
+
+ # Save using pickle
+ os.makedirs(os.path.dirname(embedding_store_path), exist_ok=True)
+ with open(embedding_store_path, "wb") as f:
+ pickle.dump(stored_data, f)
+
+ return stored_data
diff --git a/usecase/document-based-application/backend/utils/llm.py b/usecase/document-based-application/backend/utils/llm.py
new file mode 100644
index 0000000..fd3502f
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/llm.py
@@ -0,0 +1,73 @@
+from openai import OpenAI
+import os
+from typing import List
+
+# Set up the Upstage client
+UPSTAGE_API_KEY = os.getenv("UPSTAGE_API_KEY")
+if not UPSTAGE_API_KEY:
+ raise ValueError("UPSTAGE_API_KEY not set in environment.")
+
+client = OpenAI(
+ api_key=UPSTAGE_API_KEY,
+ base_url="https://api.upstage.ai/v1"
+)
+
+def chat_with_context(query: str, chunks: List[str]) -> str:
+ """
+ Sends the query and relevant context chunks to the Solar LLM.
+
+ Parameters:
+ - query: User's question
+ - chunks: List of relevant chunk texts
+
+ Returns:
+ - LLM response string
+ """
+ context_text = "\n".join([f"- Chunk {i+1}: {chunk}" for i, chunk in enumerate(chunks)])
+
+ # Format messages according to the Upstage API requirements
+ formatted_messages = [
+ {
+ "role": "system",
+ "content": (
+ "You are a helpful assistant. Use the context below to answer the user's question. "
+ "If the answer is not in the context, say you don't know."
+ )
+ },
+ {
+ "role": "user",
+ "content": f"Context:\n{context_text}\n\nQuestion: {query}"
+ }
+ ]
+
+ response = client.chat.completions.create(
+ model="solar-pro",
+ messages=formatted_messages
+ )
+
+ # Access the content directly from the response
+ return response.choices[0].message.content
+
+def chat_with_solar(message: str) -> str:
+ """
+ Simple one-message chat with the Solar model.
+
+ Args:
+ message: The user message to send to the model
+
+ Returns:
+ The model's response as a string
+ """
+ # Format messages according to the Upstage API requirements
+ formatted_messages = [
+ {"role": "system", "content": "You are a helpful assistant that provides clear and concise responses."},
+ {"role": "user", "content": message}
+ ]
+
+ response = client.chat.completions.create(
+ model="solar-pro",
+ messages=formatted_messages
+ )
+
+ # Access the content directly from the response
+ return response.choices[0].message.content
diff --git a/usecase/document-based-application/backend/utils/ocr.py b/usecase/document-based-application/backend/utils/ocr.py
new file mode 100644
index 0000000..813e2c4
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/ocr.py
@@ -0,0 +1,24 @@
+import requests
+import dotenv
+import os
+
+dotenv.load_dotenv()
+
+UPSTAGE_API_KEY = os.environ.get("UPSTAGE_API_KEY")
+if UPSTAGE_API_KEY is None:
+ raise ValueError("UPSTAGE_API_KEY not set in environment variables.")
+UPSTAGE_OCR_URL = "https://api.upstage.ai/v1/document-digitization"
+
+
+def ocr_image(file_path: str) -> str:
+ headers = {"Authorization": f"Bearer {UPSTAGE_API_KEY}"}
+ files = {"document": open(file_path, "rb")}
+ data = {"model": "ocr"}
+
+ response = requests.post(UPSTAGE_OCR_URL, headers=headers, files=files, data=data)
+
+ if response.status_code != 200:
+ raise Exception(f"OCR failed: {response.text}")
+
+ result = response.json()
+ return result.get("text") or result.get("html") or str(result)
diff --git a/usecase/document-based-application/backend/utils/parse.py b/usecase/document-based-application/backend/utils/parse.py
new file mode 100644
index 0000000..16cf825
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/parse.py
@@ -0,0 +1,28 @@
+import requests
+import dotenv
+import os
+
+dotenv.load_dotenv()
+
+UPSTAGE_API_KEY = os.environ.get("UPSTAGE_API_KEY")
+if UPSTAGE_API_KEY is None:
+ raise ValueError("UPSTAGE_API_KEY not set in environment variables.")
+UPSTAGE_PARSE_URL = "https://api.upstage.ai/v1/document-digitization"
+
+
+def parse_pdf(file_path: str) -> str:
+ headers = {"Authorization": f"Bearer {UPSTAGE_API_KEY}"}
+ files = {"document": open(file_path, "rb")}
+ data = {
+ "model": "document-parse",
+ "ocr": "force",
+ "base64_encoding": "['table']"
+ }
+
+ response = requests.post(UPSTAGE_PARSE_URL, headers=headers, files=files, data=data)
+
+ if response.status_code != 200:
+ raise Exception(f"Document parsing failed: {response.text}")
+
+ result = response.json()
+ return result.get("text") or result.get("html") or str(result)
diff --git a/usecase/document-based-application/backend/utils/retrieve.py b/usecase/document-based-application/backend/utils/retrieve.py
new file mode 100644
index 0000000..c728acc
--- /dev/null
+++ b/usecase/document-based-application/backend/utils/retrieve.py
@@ -0,0 +1,36 @@
+import numpy as np
+import os
+import pickle
+
+def retrieve_similar_chunks(query_embedding, embedding_store_path="vector_store/embedding_store.pkl", k=4):
+ """
+ Retrieves the top-k most similar chunks using dot product similarity.
+
+ Parameters:
+ - query_embedding: List[float] - the query embedding vector
+ - embedding_store_path: str - path to the saved embedding store
+ - k: int - number of most similar chunks to return
+
+ Returns:
+ - List[str] - contents of the most similar chunks
+ """
+ if not os.path.exists(embedding_store_path):
+ raise FileNotFoundError(f"Embedding store not found at '{embedding_store_path}'")
+
+ # Load stored passages and their embeddings
+ with open(embedding_store_path, "rb") as f:
+ data = pickle.load(f) # expects a list of dicts: [{'embedding': ..., 'content': ..., 'metadata': ...}, ...]
+
+ similarity_list = []
+ for item in data:
+ passage_embedding = item["embedding"]
+ similarity = np.dot(passage_embedding, query_embedding)
+ similarity_list.append((similarity, item))
+
+ # Sort by similarity, descending
+ similarity_list.sort(key=lambda x: x[0], reverse=True)
+
+ # Get top-k results
+ top_k_items = [item["content"] for _, item in similarity_list[:k]]
+
+ return top_k_items
diff --git a/usecase/document-based-application/backend/vector_store/embedding_store.pkl b/usecase/document-based-application/backend/vector_store/embedding_store.pkl
new file mode 100644
index 0000000..3c5615c
Binary files /dev/null and b/usecase/document-based-application/backend/vector_store/embedding_store.pkl differ
diff --git a/usecase/document-based-application/backend/vector_store/latest_document.pkl b/usecase/document-based-application/backend/vector_store/latest_document.pkl
new file mode 100644
index 0000000..0723b04
Binary files /dev/null and b/usecase/document-based-application/backend/vector_store/latest_document.pkl differ
diff --git a/usecase/document-based-application/frontend/app/dashboard/page.tsx b/usecase/document-based-application/frontend/app/dashboard/page.tsx
new file mode 100644
index 0000000..1ad1ab7
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/dashboard/page.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+import { useState } from "react"
+import { ChatInterface } from "@/components/chat-interface"
+import { ActionSidebar } from "@/components/action-sidebar"
+
+export default function Dashboard() {
+ const [documentType, setDocumentType] = useState(null)
+ // Always show the action sidebar
+ const [showActionSidebar, setShowActionSidebar] = useState(true)
+
+ // Function to update document type when a document is processed
+ const handleDocumentProcessed = (type: string) => {
+ setDocumentType(type)
+ }
+
+ const toggleActionSidebar = () => {
+ setShowActionSidebar(!showActionSidebar)
+ }
+
+ return (
+
+
+
+ {/* Chat Interface (full screen minus sidebar width) */}
+
+
+
+
+ {/* Action Sidebar (permanent on desktop, toggleable on mobile) */}
+
setShowActionSidebar(false)}
+ isPermanent={true}
+ />
+
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/app/globals.css b/usecase/document-based-application/frontend/app/globals.css
new file mode 100644
index 0000000..77ef0af
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/globals.css
@@ -0,0 +1,79 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 240 10% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 240 10% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 10% 3.9%;
+ --primary: 221.2 83.2% 53.3%;
+ --primary-foreground: 210 40% 98%;
+ --secondary: 240 4.8% 95.9%;
+ --secondary-foreground: 240 5.9% 10%;
+ --muted: 240 4.8% 95.9%;
+ --muted-foreground: 240 3.8% 46.1%;
+ --accent: 240 4.8% 95.9%;
+ --accent-foreground: 240 5.9% 10%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 5.9% 90%;
+ --input: 240 5.9% 90%;
+ --ring: 221.2 83.2% 53.3%;
+ --radius: 0.5rem;
+
+ /* Sidebar variables */
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+
+ .dark {
+ --background: 240 10% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 240 10% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 240 10% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 217.2 91.2% 59.8%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+ --secondary: 240 3.7% 15.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 240 3.7% 15.9%;
+ --muted-foreground: 240 5% 64.9%;
+ --accent: 240 3.7% 15.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 240 3.7% 15.9%;
+ --input: 240 3.7% 15.9%;
+ --ring: 224.3 76.3% 48%;
+
+ /* Sidebar variables */
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 0 0% 98%;
+ --sidebar-primary-foreground: 240 5.9% 10%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/usecase/document-based-application/frontend/app/layout copy.tsx b/usecase/document-based-application/frontend/app/layout copy.tsx
new file mode 100644
index 0000000..17b2ce8
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/layout copy.tsx
@@ -0,0 +1,20 @@
+import type { Metadata } from 'next'
+import './globals.css'
+
+export const metadata: Metadata = {
+ title: 'v0 App',
+ description: 'Created with v0',
+ generator: 'v0.dev',
+}
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/app/layout.tsx b/usecase/document-based-application/frontend/app/layout.tsx
new file mode 100644
index 0000000..71b196e
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/layout.tsx
@@ -0,0 +1,41 @@
+import type React from "react"
+import type { Metadata } from "next"
+import { Inter } from "next/font/google"
+import "./globals.css"
+import { ThemeProvider } from "@/components/theme-provider"
+
+const inter = Inter({ subsets: ["latin"] })
+
+export const metadata: Metadata = {
+ title: "AGI Document Assistant",
+ description: "An AGI-powered document-to-action assistant",
+ generator: 'v0.dev'
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ DocZilla - AGI-Powered Document Intelligence
+
+
+
+ {children}
+
+
+
+ )
+}
+
+
+import './globals.css'
\ No newline at end of file
diff --git a/usecase/document-based-application/frontend/app/page.tsx b/usecase/document-based-application/frontend/app/page.tsx
new file mode 100644
index 0000000..49d1bb7
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/page.tsx
@@ -0,0 +1,11 @@
+import LandingPage from "@/components/landing-page"
+
+export default function Home() {
+ // In a real application, you would check for authentication here
+ // const isAuthenticated = checkAuthStatus();
+ // if (isAuthenticated) {
+ // redirect('/dashboard');
+ // }
+
+ return
+}
diff --git a/usecase/document-based-application/frontend/app/pricing/page.tsx b/usecase/document-based-application/frontend/app/pricing/page.tsx
new file mode 100644
index 0000000..ad9b496
--- /dev/null
+++ b/usecase/document-based-application/frontend/app/pricing/page.tsx
@@ -0,0 +1,13 @@
+import { PricingCards } from "@/components/pricing/pricing-cards"
+import { PricingHeader } from "@/components/pricing/pricing-header"
+import { PricingToggle } from "@/components/pricing/pricing-toggle"
+
+export default function PricingPage() {
+ return (
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components.json b/usecase/document-based-application/frontend/components.json
new file mode 100644
index 0000000..d9ef0ae
--- /dev/null
+++ b/usecase/document-based-application/frontend/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/usecase/document-based-application/frontend/components/action-panel.tsx b/usecase/document-based-application/frontend/components/action-panel.tsx
new file mode 100644
index 0000000..bc0842f
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/action-panel.tsx
@@ -0,0 +1,179 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { CheckSquare, FolderSymlink, Send, BarChart, Table, ShieldCheck, Info, Play } from "lucide-react"
+import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
+import { Badge } from "@/components/ui/badge"
+
+interface ActionCardProps {
+ title: string
+ description: string
+ icon: React.ReactNode
+ isEnabled: boolean
+ tooltipContent: string
+ onRun: () => void
+}
+
+function ActionCard({ title, description, icon, isEnabled, tooltipContent, onRun }: ActionCardProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {tooltipContent}
+
+
+
+
+ {description}
+
+
+
+ Run
+
+
+
+ )
+}
+
+interface ActionPanelProps {
+ documentType: string | null
+}
+
+export function ActionPanel({ documentType }: ActionPanelProps) {
+ const [runningAction, setRunningAction] = useState(null)
+
+ // Define which actions are available for each document type
+ const getActionAvailability = (actionId: string) => {
+ if (!documentType) return false
+
+ switch (actionId) {
+ case "generate_action_items":
+ return documentType === "meeting_notes" || documentType === "planning"
+ case "classify_organize":
+ return true // Available for all document types
+ case "send_summary":
+ return true // Available for all document types
+ case "visualize_data":
+ return documentType === "financial"
+ case "extract_tables":
+ return documentType === "financial" || documentType === "planning"
+ case "grant_access":
+ return documentType === "financial"
+ default:
+ return false
+ }
+ }
+
+ const handleRunAction = (actionId: string) => {
+ setRunningAction(actionId)
+
+ // Simulate action running
+ setTimeout(() => {
+ setRunningAction(null)
+ }, 2000)
+ }
+
+ // Define all possible actions
+ const actions = [
+ {
+ id: "generate_action_items",
+ title: "Generate Action Items",
+ description: "Extract tasks and to-dos from your document",
+ icon: ,
+ tooltipContent:
+ "Automatically identify and extract action items, tasks, and to-dos from meeting notes or project documents.",
+ },
+ {
+ id: "classify_organize",
+ title: "Classify & Organize",
+ description: "Categorize and file your document",
+ icon: ,
+ tooltipContent:
+ "Automatically classify your document and suggest where to store it in your file system or cloud storage.",
+ },
+ {
+ id: "send_summary",
+ title: "Send Summary",
+ description: "Share key points via email or Slack",
+ icon: ,
+ tooltipContent: "Create a concise summary of your document and send it to team members via email or Slack.",
+ },
+ {
+ id: "visualize_data",
+ title: "Visualize Key Data",
+ description: "Create charts from tables and numbers",
+ icon: ,
+ tooltipContent:
+ "Extract numerical data from your document and generate visual charts and graphs for better insights.",
+ },
+ {
+ id: "extract_tables",
+ title: "Extract Tables & Charts",
+ description: "Pull structured data from your document",
+ icon: ,
+ tooltipContent:
+ "Identify and extract tables, charts, and structured data from your document for further analysis.",
+ },
+ {
+ id: "grant_access",
+ title: "Grant Access",
+ description: "Manage permissions for sensitive documents",
+ icon: ,
+ tooltipContent: "Set up appropriate access controls and permissions for sensitive or confidential documents.",
+ },
+ ]
+
+ return (
+
+
+
+
Available Actions
+ {documentType && (
+
+ {documentType.replace("_", " ")}
+
+ )}
+
+
+ {documentType ? "Select an action to perform on your document" : "Upload a document to see available actions"}
+
+
+
+
+
+ {actions.map((action) => (
+
handleRunAction(action.id)}
+ />
+ ))}
+
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/action-sidebar.tsx b/usecase/document-based-application/frontend/components/action-sidebar.tsx
new file mode 100644
index 0000000..fa25ca5
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/action-sidebar.tsx
@@ -0,0 +1,343 @@
+"use client"
+
+import type React from "react"
+import { useState, useEffect } from "react"
+import {
+ X,
+ CheckSquare,
+ FolderSymlink,
+ Send,
+ BarChart,
+ Table,
+ ShieldCheck,
+ Info,
+ Play,
+ Clock,
+ FileText,
+ CalendarClock,
+ ClipboardList,
+} from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { motion, AnimatePresence } from "framer-motion"
+import { Separator } from "@/components/ui/separator"
+
+interface ActionSidebarProps {
+ documentType: string | null
+ isOpen: boolean
+ onClose: () => void
+ isPermanent?: boolean
+}
+
+interface Action {
+ id: string
+ title: string
+ description: string
+ icon: React.ReactNode
+ isEnabled: boolean
+ tooltipContent: string
+}
+
+interface ActionGroup {
+ title: string
+ actions: Action[]
+}
+
+export function ActionSidebar({ documentType, isOpen, onClose, isPermanent = false }: ActionSidebarProps) {
+ const [runningAction, setRunningAction] = useState(null)
+ const [isMobile, setIsMobile] = useState(false)
+
+ useEffect(() => {
+ const checkMobile = () => {
+ setIsMobile(window.innerWidth < 768)
+ }
+
+ // ะัะพะฒะตััะตะผ ะฟัะธ ะผะพะฝัะธัะพะฒะฐะฝะธะธ
+ checkMobile()
+
+ // ะะพะฑะฐะฒะปัะตะผ ัะปััะฐัะตะปั ะธะทะผะตะฝะตะฝะธั ัะฐะทะผะตัะฐ ะพะบะฝะฐ
+ window.addEventListener('resize', checkMobile)
+
+ // ะฃะฑะธัะฐะตะผ ัะปััะฐัะตะปั ะฟัะธ ัะฐะทะผะพะฝัะธัะพะฒะฐะฝะธะธ
+ return () => window.removeEventListener('resize', checkMobile)
+ }, [])
+
+ // Define which actions are available for each document type
+ const getActionAvailability = (actionId: string) => {
+ if (!documentType) return false
+
+ switch (actionId) {
+ case "generate_action_items":
+ return documentType === "meeting_notes" || documentType === "planning"
+ case "classify_organize":
+ return true // Available for all document types
+ case "send_summary":
+ return true // Available for all document types
+ case "visualize_data":
+ return documentType === "financial"
+ case "extract_tables":
+ return documentType === "financial" || documentType === "planning"
+ case "grant_access":
+ return documentType === "financial"
+ case "create_timeline":
+ return documentType === "planning"
+ case "summarize":
+ return true // Available for all document types
+ case "checklist":
+ return true // Available for all document types
+ default:
+ return false
+ }
+ }
+
+ const handleRunAction = async (actionId: string) => {
+ setRunningAction(actionId)
+
+ try {
+ if (actionId === "send_summary") {
+ const response = await fetch("http://127.0.0.1:8000/send-slack", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ },
+ mode: "cors",
+ body: JSON.stringify({
+ message: "New document summary has been generated and shared with the team",
+ }),
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ detail: "Failed to send Slack message" }))
+ throw new Error(errorData.detail || "Failed to send Slack message")
+ }
+
+ const data = await response.json()
+ console.log("Slack message sent:", data)
+ } else if (actionId === "checklist") {
+ // Call the compare-policy endpoint
+ const response = await fetch("http://127.0.0.1:8000/compare-policy", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ },
+ mode: "cors",
+ })
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({ detail: "Failed to compare policy" }))
+ throw new Error(errorData.detail || "Failed to compare policy")
+ }
+
+ const data = await response.json()
+ console.log("Policy comparison completed:", data)
+
+ // Dispatch a custom event to notify the chat interface
+ const event = new CustomEvent('policyComparisonComplete', {
+ detail: {
+ formattedResults: data.formatted_results
+ }
+ })
+ window.dispatchEvent(event)
+ }
+
+ // Simulate other actions running
+ setTimeout(() => {
+ setRunningAction(null)
+ }, 2000)
+ } catch (error) {
+ console.error("Error running action:", error)
+ setRunningAction(null)
+ }
+ }
+
+ // Define all possible actions grouped by category
+ const actionGroups: ActionGroup[] = [
+ {
+ title: "Analysis",
+ actions: [
+ {
+ id: "summarize",
+ title: "Summarize Document",
+ description: "Create a concise summary of the key points",
+ icon: ,
+ isEnabled: getActionAvailability("summarize"),
+ tooltipContent: "Generate a comprehensive summary of the document's main points and insights.",
+ },
+ {
+ id: "checklist",
+ title: "Check Against Internal Policy",
+ description: "Generate a checklist and flag deviations from company standards",
+ icon: ,
+ isEnabled: getActionAvailability("checklist"),
+ tooltipContent: "Generate a comprehensive summary of the document's main points and insights.",
+ },
+
+
+ ],
+ },
+ {
+ title: "Generation",
+ actions: [
+ {
+ id: "classify_organize",
+ title: "Classify & Organize",
+ description: "Categorize and file your document",
+ icon: ,
+ isEnabled: getActionAvailability("classify_organize"),
+ tooltipContent:
+ "Automatically classify your document and place it in Google Drive storage.",
+ },
+ {
+ id: "extract_tables",
+ title: "Extract Tables & Charts",
+ description: "Pull structured data from your document",
+ icon: ,
+ isEnabled: getActionAvailability("extract_tables"),
+ tooltipContent:
+ "Identify and extract tables, charts, and structured data from your document for further analysis.",
+ },
+ ],
+ },
+ {
+ title: "Workflow",
+ actions: [
+ {
+ id: "send_summary",
+ title: "Send Summary",
+ description: "Share key points via Slack to the team",
+ icon: ,
+ isEnabled: getActionAvailability("send_summary"),
+ tooltipContent: "Create a concise summary of your document and send it to team members Slack.",
+ },
+ {
+ id: "generate_action_items",
+ title: "Extract Action Items",
+ description: "Identify tasks from your document, and turn findings into Notion to-do items",
+ icon: ,
+ isEnabled: getActionAvailability("generate_action_items"),
+ tooltipContent: "Automatically identify and extract action items, tasks, and to-dos from meeting notes or project documents.",
+ },
+ {
+ id: "setting_calendar_event",
+ title: "Set Calendar Event",
+ description: "Add renewal/termination dates to Google Calendar",
+ icon: ,
+ isEnabled: getActionAvailability("setting_calendar_event"),
+ tooltipContent: "Automatically identify and extract action items, tasks, and to-dos from meeting notes or project documents.",
+ },
+ ],
+ },
+ ]
+
+ return (
+
+ {isOpen && (
+ <>
+ {/* Mobile overlay - only show when not permanent and on mobile */}
+ {!isPermanent && (
+
+ )}
+
+ {/* Sidebar */}
+
+
+
+
Available Actions
+ {documentType && (
+
+ {documentType.replace("_", " ")}
+
+ )}
+
+ {/* Only show close button when not permanent or on mobile */}
+ {(!isPermanent || isMobile) && (
+
+
+
+ )}
+
+
+ {/* Rest of the sidebar content remains the same */}
+
+
+ {actionGroups.map((group) => (
+
+
+
{group.title}
+
+
+
+ {group.actions.map((action) => (
+
+
+
+
+ {action.icon}
+
+
+
{action.title}
+
{action.description}
+
+
+
+
+
+
handleRunAction(action.id)}
+ >
+ {runningAction === action.id ? (
+ <>Processing...>
+ ) : (
+ <>
+ Run
+ >
+ )}
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+ >
+ )}
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/app-sidebar.tsx b/usecase/document-based-application/frontend/components/app-sidebar.tsx
new file mode 100644
index 0000000..041c5c7
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/app-sidebar.tsx
@@ -0,0 +1,114 @@
+"use client"
+
+import { useState } from "react"
+import { FileText, FolderOpen, Home, Settings } from "lucide-react"
+import {
+ Sidebar,
+ SidebarContent,
+ SidebarFooter,
+ SidebarGroup,
+ SidebarGroupContent,
+ SidebarGroupLabel,
+ SidebarHeader,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarTrigger,
+} from "@/components/ui/sidebar"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+
+// Mock recent documents data
+const recentDocuments = [
+ { id: 1, name: "Meeting Notes - Product Team", date: "2 hours ago", type: "meeting_notes" },
+ { id: 2, name: "Q2 Financial Report", date: "Yesterday", type: "financial" },
+ { id: 3, name: "Project Roadmap 2025", date: "3 days ago", type: "planning" },
+ { id: 4, name: "Customer Feedback Summary", date: "1 week ago", type: "feedback" },
+]
+
+export function AppSidebar() {
+ const [activeItem, setActiveItem] = useState("home")
+
+ return (
+
+
+
+
+
+
+
+
+ Navigation
+
+
+
+ setActiveItem("home")}
+ tooltip="Home"
+ >
+
+ Home
+
+
+
+ setActiveItem("documents")}
+ tooltip="All Documents"
+ >
+
+ All Documents
+
+
+
+
+
+
+
+ Recent Documents
+
+
+ {recentDocuments.map((doc) => (
+
+
+
+
+ {doc.name}
+ {doc.date}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
+
+ JD
+
+
+ John Doe
+ john@example.com
+
+
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/chat-interface.tsx b/usecase/document-based-application/frontend/components/chat-interface.tsx
new file mode 100644
index 0000000..1e6a5e5
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/chat-interface.tsx
@@ -0,0 +1,472 @@
+"use client"
+
+import type React from "react"
+
+import { useState, useRef, useEffect } from "react"
+import { FileUp, Send, FileText, ImageIcon, Bot, User, ChevronRight } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { FileUploader } from "@/components/file-uploader"
+import { motion, AnimatePresence } from "framer-motion"
+import { sendChatMessage } from "@/services/chat"
+import { uploadDocument } from "@/services/upload"
+
+interface ChatMessage {
+ id: string
+ role: "user" | "assistant" | "system"
+ content: string
+ timestamp: Date
+ attachments?: {
+ type: "pdf" | "image"
+ name: string
+ size: string
+ }[]
+ suggestion?: {
+ text: string
+ action: string
+ }
+}
+
+interface ChatInterfaceProps {
+ onDocumentProcessed: (documentType: string) => void
+ documentType: string | null
+ onToggleActionSidebar: () => void
+ showActionSidebar: boolean
+}
+
+export function ChatInterface({
+ onDocumentProcessed,
+ documentType,
+ onToggleActionSidebar,
+ showActionSidebar,
+}: ChatInterfaceProps) {
+ const [messages, setMessages] = useState([
+ {
+ id: "1",
+ role: "assistant",
+ content:
+ "Hello! I'm your document assistant. Upload a document, and I'll help you analyze it and suggest actions.",
+ timestamp: new Date(),
+ },
+ ])
+ const [input, setInput] = useState("")
+ const [isUploading, setIsUploading] = useState(false)
+ const [showUploader, setShowUploader] = useState(false)
+ const messagesEndRef = useRef(null)
+ const chatContainerRef = useRef(null)
+ const [isSending, setIsSending] = useState(false)
+ const isSendingRef = useRef(false)
+ const [currentDocument, setCurrentDocument] = useState<{ id?: string; content?: string } | null>(null)
+ const [uploadSuccess, setUploadSuccess] = useState(false)
+ const [uploadAnnouncement, setUploadAnnouncement] = useState<{ text: string; visible: boolean } | null>(null)
+
+ // Scroll to bottom when messages change
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
+ }, [messages])
+
+ // Listen for policy comparison complete event
+ useEffect(() => {
+ const handlePolicyComparisonComplete = (event: CustomEvent) => {
+ const { formattedResults } = event.detail;
+
+ // Add the policy comparison results to the chat
+ const policyMessage: ChatMessage = {
+ id: `${Date.now()}-policy-comparison`,
+ role: "assistant",
+ content: formattedResults,
+ timestamp: new Date(),
+ };
+
+ setMessages(prev => [...prev, policyMessage]);
+ };
+
+ // Add event listener
+ window.addEventListener('policyComparisonComplete', handlePolicyComparisonComplete as EventListener);
+
+ // Clean up
+ return () => {
+ window.removeEventListener('policyComparisonComplete', handlePolicyComparisonComplete as EventListener);
+ };
+ }, []);
+
+ const handleSendMessage = async () => {
+ if (!input.trim() || isSendingRef.current) return
+
+ isSendingRef.current = true
+ setIsSending(true)
+
+ const userMessage: ChatMessage = {
+ id: `${Date.now()}-user`,
+ role: "user",
+ content: input,
+ timestamp: new Date(),
+ }
+
+ // Show user message immediately
+ setMessages(prev => [...prev, userMessage])
+ setInput("")
+
+ try {
+ // Include document info in the chat request if available
+ const chatPayload = {
+ messages: [...messages, userMessage],
+ documentId: currentDocument?.id,
+ documentContent: currentDocument?.content
+ }
+
+ // Send messages to backend and get response
+ const updatedMessages = await sendChatMessage(chatPayload)
+
+ // Update messages with the complete response
+ setMessages(updatedMessages)
+ } catch (error) {
+ console.error("Error sending message:", error)
+ // Add error message to chat
+ const errorMessage: ChatMessage = {
+ id: `${Date.now()}-error`,
+ role: "assistant",
+ content: "Sorry, I encountered an error while processing your message. Please try again.",
+ timestamp: new Date(),
+ }
+ setMessages(prev => [...prev, errorMessage])
+ } finally {
+ setIsSending(false)
+ isSendingRef.current = false
+ }
+ }
+
+
+
+ const handleFileUpload = async (files: File[]) => {
+ if (!files.length) return
+
+ const file = files[0]
+ const fileSize = (file.size / 1024).toFixed(0) + " KB"
+ const fileType = file.name.endsWith(".pdf") ? "pdf" : "image"
+
+ setIsUploading(true)
+ setShowUploader(false)
+ setUploadSuccess(false)
+
+ // Create a message indicating file upload
+ const uploadMessage: ChatMessage = {
+ id: `${Date.now()}-user-upload`,
+ role: "user",
+ content: "",
+ timestamp: new Date(),
+ attachments: [
+ {
+ type: fileType,
+ name: file.name,
+ size: fileSize,
+ },
+ ],
+ }
+ setMessages((prev) => [...prev, uploadMessage])
+
+ try {
+ const uploadResponse = await uploadDocument(file)
+
+ // if (!uploadResponse.success || !uploadResponse.documentId) {
+ // throw new Error("Upload failed or documentId missing")
+ // }
+
+ setCurrentDocument({
+ id: uploadResponse.documentId,
+ content: uploadResponse.content,
+ })
+ setUploadSuccess(true)
+
+ // Determine document type
+ let docType = "document"
+ let suggestion = "Would you like me to summarize this document?"
+ let assistantContent = "I've analyzed your document and extracted the key information."
+
+ const name = file.name.toLowerCase()
+ if (name.includes("meeting") || name.includes("notes")) {
+ docType = "meeting_notes"
+ suggestion = "Would you like me to extract action items from these meeting notes?"
+ assistantContent =
+ "I've analyzed your meeting notes and identified action items and decisions discussed."
+ } else if (name.includes("financial") || name.includes("report")) {
+ docType = "financial"
+ suggestion = "Would you like me to visualize the key financial data in this report?"
+ assistantContent =
+ "I've analyzed your financial report. It includes revenue figures, expenses, and projections."
+ } else if (name.includes("project") || name.includes("roadmap")) {
+ docType = "planning"
+ suggestion = "Would you like me to create a timeline from this project document?"
+ assistantContent =
+ "I've analyzed your project planning doc with key milestones and launch dates."
+ }
+
+ onDocumentProcessed(docType)
+
+ const systemMessage: ChatMessage = {
+ id: `${Date.now()}-system`,
+ role: "system",
+ content: `${file.name} has been uploaded successfully.`,
+ timestamp: new Date(),
+ }
+
+ const assistantMessage: ChatMessage = {
+ id: `${Date.now()}-assistant`,
+ role: "assistant",
+ content: assistantContent,
+ timestamp: new Date(),
+ suggestion: {
+ text: suggestion,
+ action: docType === "meeting_notes"
+ ? "extract_action_items"
+ : docType === "financial"
+ ? "visualize_data"
+ : docType === "planning"
+ ? "create_timeline"
+ : "summarize",
+ },
+ }
+
+ setMessages((prev) => [...prev, systemMessage, assistantMessage])
+ } catch (err) {
+ console.error("Upload error:", err)
+
+ // const errorMessage: ChatMessage = {
+ // id: `${Date.now()}-upload-error`,
+ // role: "assistant",
+ // content: "",
+ // timestamp: new Date(),
+ // }
+ // setMessages((prev) => [...prev, errorMessage])
+ } finally {
+ setIsUploading(false)
+ setTimeout(() => setUploadSuccess(false), 2000)
+ }
+ }
+
+
+ const handleSuggestionAction = async (action: string) => {
+ // Add user message accepting the suggestion
+ const userMessage: ChatMessage = {
+ id: Date.now().toString(),
+ role: "user",
+ content: "Yes, please do that.",
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, userMessage])
+
+ // Simulate AI processing the action
+ setTimeout(async () => {
+ let responseContent = ""
+
+ switch (action) {
+ case "extract_action_items":
+ responseContent =
+ "I've extracted the following action items from your meeting notes:\n\n1. @John to finalize the Q3 product roadmap by Friday\n2. @Sarah to schedule user testing sessions for the new feature\n3. @Team to review the competitor analysis before next meeting\n4. @Michael to update the project timeline in Jira"
+ break
+ case "visualize_data":
+ responseContent =
+ "I've created visualizations based on your financial data. The charts show a 15% increase in revenue compared to last quarter, with marketing expenses decreasing by 8%. Would you like me to send these visualizations to your team?"
+ break
+ case "create_timeline":
+ responseContent =
+ "I've created a project timeline based on your planning document. The critical path shows the product launch is scheduled for October 15th, with beta testing beginning on September 1st. Would you like me to export this timeline to your project management tool?"
+ break
+ case "summarize":
+ const response = await fetch("http://127.0.0.1:8000/send-slack?only_summary=true", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({}),
+ })
+ const data = await response.json()
+ responseContent = data.summary
+ break
+ default:
+ responseContent =
+ "I've processed your request. Is there anything else you'd like me to do with this document?"
+ }
+
+ const assistantMessage: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: "assistant",
+ content: responseContent,
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, assistantMessage])
+ }, 1500)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ handleSendMessage()
+ }
+ }
+
+ return (
+
+
+
DocZilla
+
+
+ New Chat
+
+ {/* Add a mobile-only button to toggle the action sidebar */}
+
+ Actions
+
+
+
+
+
+
+
+ {messages.map((message, index) => (
+
+ {message.role === "system" ? (
+ // System announcement style
+
+
+
+ {message.content}
+
+
+ ) : (
+ // Regular message style
+
+
+ {message.role === "assistant" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {message.attachments?.map((attachment, index) => (
+
+ {attachment.type === "pdf" ? (
+
+ ) : (
+
+ )}
+
+ {attachment.name}
+ {attachment.size}
+
+
+ ))}
+
{message.content}
+
+ {message.suggestion && (
+
+
{message.suggestion.text}
+
+ handleSuggestionAction(message.suggestion!.action)}
+ >
+ Yes
+
+
+ More actions
+
+
+
+ )}
+
+
+ {message.timestamp.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+ )}
+
+ ))}
+
+
+
+
+
+ {showUploader && (
+
+ setShowUploader(false)} isUploading={isUploading} />
+
+ )}
+
+
+
+
+
setShowUploader(!showUploader)}
+ className={`shrink-0 rounded-full relative ${
+ uploadSuccess ? "bg-green-500/10 border-green-500" : ""
+ }`}
+ >
+
+ {uploadSuccess && (
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/chat-panel.tsx b/usecase/document-based-application/frontend/components/chat-panel.tsx
new file mode 100644
index 0000000..cfaf8f4
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/chat-panel.tsx
@@ -0,0 +1,221 @@
+"use client"
+
+import type React from "react"
+
+import { useState, useRef, useEffect } from "react"
+import { FileUp, Send, FileText, ImageIcon, Bot, User } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { FileUploader } from "@/components/file-uploader"
+
+interface ChatMessage {
+ id: string
+ role: "user" | "assistant"
+ content: string
+ timestamp: Date
+ attachments?: {
+ type: "pdf" | "image"
+ name: string
+ size: string
+ }[]
+}
+
+interface ChatPanelProps {
+ onDocumentProcessed: (documentType: string) => void
+}
+
+export function ChatPanel({ onDocumentProcessed }: ChatPanelProps) {
+ const [messages, setMessages] = useState([
+ {
+ id: "1",
+ role: "assistant",
+ content:
+ "Hello! I'm your document assistant. Upload a document, and I'll help you analyze it and suggest actions.",
+ timestamp: new Date(),
+ },
+ ])
+ const [input, setInput] = useState("")
+ const [isUploading, setIsUploading] = useState(false)
+ const [showUploader, setShowUploader] = useState(false)
+ const messagesEndRef = useRef(null)
+
+ // Scroll to bottom when messages change
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
+ }, [messages])
+
+ const handleSendMessage = () => {
+ if (!input.trim()) return
+
+ // Add user message
+ const userMessage: ChatMessage = {
+ id: Date.now().toString(),
+ role: "user",
+ content: input,
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, userMessage])
+ setInput("")
+
+ // Simulate AI response
+ setTimeout(() => {
+ const assistantMessage: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: "assistant",
+ content: "I'm analyzing your request. Is there a specific document you'd like me to help with?",
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, assistantMessage])
+ }, 1000)
+ }
+
+ const handleFileUpload = (files: File[]) => {
+ setIsUploading(true)
+ setShowUploader(false)
+
+ // Simulate file upload and processing
+ setTimeout(() => {
+ const file = files[0]
+ const fileSize = (file.size / 1024).toFixed(0) + " KB"
+ const fileType = file.name.endsWith(".pdf") ? "pdf" : "image"
+
+ // Add user message with attachment
+ const userMessage: ChatMessage = {
+ id: Date.now().toString(),
+ role: "user",
+ content: "I've uploaded a document for analysis.",
+ timestamp: new Date(),
+ attachments: [
+ {
+ type: fileType as "pdf" | "image",
+ name: file.name,
+ size: fileSize,
+ },
+ ],
+ }
+
+ setMessages((prev) => [...prev, userMessage])
+ setIsUploading(false)
+
+ // Simulate AI processing and response
+ setTimeout(() => {
+ // Determine document type based on filename (in a real app, this would be done by AI)
+ let documentType = "unknown"
+ if (file.name.toLowerCase().includes("meeting") || file.name.toLowerCase().includes("notes")) {
+ documentType = "meeting_notes"
+ } else if (file.name.toLowerCase().includes("financial") || file.name.toLowerCase().includes("report")) {
+ documentType = "financial"
+ } else if (file.name.toLowerCase().includes("project") || file.name.toLowerCase().includes("roadmap")) {
+ documentType = "planning"
+ }
+
+ // Update parent component with document type
+ onDocumentProcessed(documentType)
+
+ // Generate appropriate response based on document type
+ let responseContent = ""
+ if (documentType === "meeting_notes") {
+ responseContent =
+ "I've analyzed your meeting notes. I can identify several action items and key decisions. Would you like me to extract the action items or send a summary to your team?"
+ } else if (documentType === "financial") {
+ responseContent =
+ "I've analyzed your financial document. I can extract key financial data, create visualizations, or classify this for your records. What would you like me to do?"
+ } else if (documentType === "planning") {
+ responseContent =
+ "I've analyzed your project planning document. I can help organize tasks, extract deadlines, or create a summary. How would you like to proceed?"
+ } else {
+ responseContent = "I've analyzed your document. What would you like me to do with it?"
+ }
+
+ const assistantMessage: ChatMessage = {
+ id: (Date.now() + 1).toString(),
+ role: "assistant",
+ content: responseContent,
+ timestamp: new Date(),
+ }
+
+ setMessages((prev) => [...prev, assistantMessage])
+ }, 1500)
+ }, 2000)
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault()
+ handleSendMessage()
+ }
+ }
+
+ return (
+
+
+
Document Assistant
+
Upload documents and get AI-powered insights and actions
+
+
+
+
+ {messages.map((message) => (
+
+
+
+ {message.role === "assistant" ? : }
+
+
+ {message.attachments?.map((attachment, index) => (
+
+ {attachment.type === "pdf" ?
:
}
+
+ {attachment.name}
+ {attachment.size}
+
+
+ ))}
+
{message.content}
+
+ {message.timestamp.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
+
+
+
+
+ ))}
+
+
+
+
+ {showUploader && (
+
+ setShowUploader(false)} isUploading={isUploading} />
+
+ )}
+
+
+
+ setShowUploader(!showUploader)} className="shrink-0">
+
+
+
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/file-uploader.tsx b/usecase/document-based-application/frontend/components/file-uploader.tsx
new file mode 100644
index 0000000..29f0f32
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/file-uploader.tsx
@@ -0,0 +1,124 @@
+"use client"
+
+import type React from "react"
+
+import { useState, useRef } from "react"
+import { Upload, X, Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Card } from "@/components/ui/card"
+
+interface FileUploaderProps {
+ onUpload: (files: File[]) => void
+ onCancel: () => void
+ isUploading: boolean
+}
+
+export function FileUploader({ onUpload, onCancel, isUploading }: FileUploaderProps) {
+ const [dragActive, setDragActive] = useState(false)
+ const [selectedFiles, setSelectedFiles] = useState([])
+ const fileInputRef = useRef(null)
+
+ const handleDrag = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ if (e.type === "dragenter" || e.type === "dragover") {
+ setDragActive(true)
+ } else if (e.type === "dragleave") {
+ setDragActive(false)
+ }
+ }
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+ setDragActive(false)
+
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+ const filesArray = Array.from(e.dataTransfer.files)
+ setSelectedFiles(filesArray)
+ }
+ }
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files.length > 0) {
+ const filesArray = Array.from(e.target.files)
+ setSelectedFiles(filesArray)
+ }
+ }
+
+ const handleUpload = () => {
+ if (selectedFiles.length > 0) {
+ onUpload(selectedFiles)
+ }
+ }
+
+ const openFileSelector = () => {
+ fileInputRef.current?.click()
+ }
+
+ return (
+
+
+
+
+
+
+
+ {isUploading ? (
+
+
+
Uploading document...
+
+ ) : selectedFiles.length > 0 ? (
+
+
+ {selectedFiles.map((file, index) => (
+
+
+
+
+
+
+ {file.name}
+ {(file.size / 1024).toFixed(0)} KB
+
+
+
+ ))}
+
+
+ setSelectedFiles([])}>
+ Clear
+
+ Upload
+
+
+ ) : (
+
+
+
+
+
+
Drag and drop your files here
+
Supports PDF, PNG, and JPG files
+
+
Select Files
+
+ )}
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/landing-page.tsx b/usecase/document-based-application/frontend/components/landing-page.tsx
new file mode 100644
index 0000000..7da5192
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/landing-page.tsx
@@ -0,0 +1,372 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useRouter } from "next/navigation"
+import { FileText, ArrowRight, CheckCircle, ChevronRight, Sparkles, Shield, BarChart, Clock, Users } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { LoginForm } from "@/components/login-form"
+import { SignupForm } from "@/components/signup-form"
+import { motion } from "framer-motion"
+
+export default function LandingPage() {
+ const [activeForm, setActiveForm] = useState<"login" | "signup" | null>(null)
+ const router = useRouter()
+ const [mounted, setMounted] = useState(false)
+
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ const handleDemoClick = () => {
+ router.push("/dashboard")
+ }
+
+ const handleSuccessfulAuth = () => {
+ router.push("/dashboard")
+ }
+
+ const handlePricingClick = () => {
+ router.push("/pricing")
+ }
+
+ const features = [
+ {
+ title: "Smart Document Analysis",
+ description: "Upload any document and get instant insights with our advanced AI analysis.",
+ icon: ,
+ },
+ {
+ title: "Action Item Extraction",
+ description: "Automatically identify and extract action items from meeting notes and documents.",
+ icon: ,
+ },
+ {
+ title: "Data Visualization",
+ description: "Transform tables and numbers into beautiful, insightful charts and graphs.",
+ icon: ,
+ },
+ {
+ title: "Secure Document Handling",
+ description: "Enterprise-grade security ensures your sensitive documents remain protected.",
+ icon: ,
+ },
+ {
+ title: "Time-Saving Automation",
+ description: "Save hours of manual work with automated document processing and organization.",
+ icon: ,
+ },
+ {
+ title: "Team Collaboration",
+ description: "Share insights and summaries with your team for better collaboration.",
+ icon: ,
+ },
+ ]
+
+ return (
+
+ {/* Background gradient */}
+
+
+
+ {/* Header */}
+
+
+
+
+
+
+ Features
+
+
+ Pricing
+
+
+ Documentation
+
+
+
setActiveForm("login")} className="text-sm">
+ Log in
+
+
setActiveForm("signup")} className="text-sm">
+ Sign up
+
+
+
+
+
+ {/* Main content */}
+
+ {/* Hero section */}
+
+
+
+
+
+ {mounted && (
+
+
+ Make Documents Work for You with AGI-Powered Understanding
+
+
+ Upload any document and let our AGI assistant analyze, extract, and take action on your behalf.
+ Save hours of manual work.
+
+
+
setActiveForm("signup")}
+ className="rounded-full px-8 shadow-lg hover:shadow-primary/20 hover:scale-105 transition-all duration-200 gap-2"
+ >
+ Get Started
+
+
+ Try Demo
+
+
+
+ )}
+
+ {mounted && (
+
+
+
+
+
+
+
+
+
Intelligent Document Processing
+
+ Upload PDFs, images, and documents for instant analysis and actionable insights.
+
+
+
+
+
+ )}
+
+
+
+
+ {/* Features section */}
+
+
+
+
+
Powerful Features
+
+ Our AI-powered platform helps you extract value from your documents with minimal effort.
+
+
+
+ {features.map((feature, index) => (
+
+
+ {feature.title}
+ {feature.description}
+
+ ))}
+
+
+
+
+ {/* CTA section */}
+
+
+
+
Ready to transform your document workflow?
+
+ Join thousands of professionals who save hours every week with DocZilla.
+
+
setActiveForm("signup")}
+ className="rounded-full px-8 shadow-lg hover:shadow-primary/20 hover:scale-105 transition-all duration-200 gap-2"
+ >
+ Start Free Trial
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
AI-powered document intelligence for modern teams.
+
+
+
+
+
+
+
ยฉ 2025 DocZilla. All rights reserved.
+
+
+
+
+
+ {/* Auth modal */}
+ {activeForm && (
+
+
+ setActiveForm(null)}
+ >
+ โ
+
+ {activeForm === "login" ? (
+ <>
+ Log in to DocZilla
+
+
+
+ Don't have an account?{" "}
+ setActiveForm("signup")}>
+ Sign up
+
+
+
+ >
+ ) : (
+ <>
+ Create your account
+
+
+
+ Already have an account?{" "}
+ setActiveForm("login")}>
+ Log in
+
+
+
+ >
+ )}
+
+
+ )}
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/login-form.tsx b/usecase/document-based-application/frontend/components/login-form.tsx
new file mode 100644
index 0000000..693aac8
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/login-form.tsx
@@ -0,0 +1,105 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { useRouter } from "next/navigation"
+import { Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Checkbox } from "@/components/ui/checkbox"
+
+interface LoginFormProps {
+ onSuccess: () => void
+}
+
+export function LoginForm({ onSuccess }: LoginFormProps) {
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState("")
+ const router = useRouter()
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+ setIsLoading(true)
+
+ // Simulate API call
+ try {
+ // In a real app, you would call your authentication API here
+ await new Promise((resolve) => setTimeout(resolve, 1500))
+
+ // For demo purposes, we'll just accept any input
+ onSuccess()
+ } catch (err) {
+ setError("Invalid email or password. Please try again.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/pricing/pricing-cards.tsx b/usecase/document-based-application/frontend/components/pricing/pricing-cards.tsx
new file mode 100644
index 0000000..b1345d0
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/pricing/pricing-cards.tsx
@@ -0,0 +1,128 @@
+"use client"
+
+import { useState } from "react"
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Check } from "lucide-react"
+
+const pricingPlans = [
+ {
+ name: "Free",
+ emoji: "๐ธ",
+ price: {
+ monthly: 0,
+ yearly: 0,
+ },
+ description: "For individuals just getting started",
+ features: ["10 documents per month", "Summary & Slack integration", "Basic classification", "Limited support"],
+ cta: "Start for Free",
+ popular: false,
+ },
+ {
+ name: "Pro",
+ emoji: "๐",
+ price: {
+ monthly: 29,
+ yearly: 279,
+ },
+ description: "For professionals and small teams",
+ features: ["100 documents", "Notion, Calendar, and Slack actions", "Smart search + timeline", "Email support"],
+ cta: "Upgrade to Pro",
+ popular: true,
+ },
+ {
+ name: "Team",
+ emoji: "๐งโ๐คโ๐ง",
+ price: {
+ monthly: 99,
+ yearly: 949,
+ },
+ description: "Includes 20 users",
+ features: ["500 documents", "Shared workspace", "Drive/Slack/Notion sync", "Document memory across users"],
+ cta: "Start Team Plan",
+ popular: false,
+ },
+ {
+ name: "Enterprise",
+ emoji: "๐ข",
+ price: {
+ monthly: null,
+ yearly: null,
+ },
+ description: "For large organizations with custom needs",
+ features: [
+ "Unlimited documents",
+ "Dedicated AI instance (custom RAG)",
+ "SOC2-compliant audit logs",
+ "Onboarding support & API SLA",
+ ],
+ cta: "Contact Sales",
+ popular: false,
+ },
+]
+
+export function PricingCards() {
+ const [isYearly, setIsYearly] = useState(false)
+
+ return (
+
+ {pricingPlans.map((plan) => (
+
+ {plan.popular && (
+
+
+ Most Popular
+
+
+ )}
+
+ {plan.emoji}
+ {plan.name}
+ {plan.description}
+
+
+
+ {plan.price.monthly === null ? (
+
Custom pricing
+ ) : (
+
+ ${isYearly ? plan.price.yearly : plan.price.monthly}
+
+ {plan.price.monthly > 0 && (isYearly ? "/year" : "/month per user")}
+
+
+ )}
+
+
+ {plan.features.map((feature) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+ {plan.cta}
+
+
+
+ ))}
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/pricing/pricing-header.tsx b/usecase/document-based-application/frontend/components/pricing/pricing-header.tsx
new file mode 100644
index 0000000..c2da0f1
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/pricing/pricing-header.tsx
@@ -0,0 +1,10 @@
+export function PricingHeader() {
+ return (
+
+
Find the right plan for your team
+
+ Choose the perfect plan for your needs. Upgrade or downgrade at any time.
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/pricing/pricing-toggle.tsx b/usecase/document-based-application/frontend/components/pricing/pricing-toggle.tsx
new file mode 100644
index 0000000..91db638
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/pricing/pricing-toggle.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import { useState } from "react"
+import { Switch } from "@/components/ui/switch"
+import { Label } from "@/components/ui/label"
+
+export function PricingToggle() {
+ const [isYearly, setIsYearly] = useState(false)
+
+ return (
+
+
+ Monthly
+
+
+
+ Yearly
+ {isYearly && (
+ Save 20%
+ )}
+
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/signup-form.tsx b/usecase/document-based-application/frontend/components/signup-form.tsx
new file mode 100644
index 0000000..0859da2
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/signup-form.tsx
@@ -0,0 +1,112 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { Loader2 } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Checkbox } from "@/components/ui/checkbox"
+
+interface SignupFormProps {
+ onSuccess: () => void
+}
+
+export function SignupForm({ onSuccess }: SignupFormProps) {
+ const [name, setName] = useState("")
+ const [email, setEmail] = useState("")
+ const [password, setPassword] = useState("")
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState("")
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+ setIsLoading(true)
+
+ // Simulate API call
+ try {
+ // In a real app, you would call your registration API here
+ await new Promise((resolve) => setTimeout(resolve, 1500))
+
+ // For demo purposes, we'll just accept any input
+ onSuccess()
+ } catch (err) {
+ setError("There was an error creating your account. Please try again.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/usecase/document-based-application/frontend/components/theme-provider.tsx b/usecase/document-based-application/frontend/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/usecase/document-based-application/frontend/components/ui/accordion.tsx b/usecase/document-based-application/frontend/components/ui/accordion.tsx
new file mode 100644
index 0000000..24c788c
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/usecase/document-based-application/frontend/components/ui/alert-dialog.tsx b/usecase/document-based-application/frontend/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..25e7b47
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/usecase/document-based-application/frontend/components/ui/alert.tsx b/usecase/document-based-application/frontend/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/usecase/document-based-application/frontend/components/ui/aspect-ratio.tsx b/usecase/document-based-application/frontend/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/usecase/document-based-application/frontend/components/ui/avatar.tsx b/usecase/document-based-application/frontend/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/usecase/document-based-application/frontend/components/ui/badge.tsx b/usecase/document-based-application/frontend/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/usecase/document-based-application/frontend/components/ui/breadcrumb.tsx b/usecase/document-based-application/frontend/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/usecase/document-based-application/frontend/components/ui/button.tsx b/usecase/document-based-application/frontend/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/usecase/document-based-application/frontend/components/ui/calendar.tsx b/usecase/document-based-application/frontend/components/ui/calendar.tsx
new file mode 100644
index 0000000..61d2b45
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/usecase/document-based-application/frontend/components/ui/card.tsx b/usecase/document-based-application/frontend/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/usecase/document-based-application/frontend/components/ui/carousel.tsx b/usecase/document-based-application/frontend/components/ui/carousel.tsx
new file mode 100644
index 0000000..ec505d0
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+
+ Previous slide
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+
+ Next slide
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/usecase/document-based-application/frontend/components/ui/chart.tsx b/usecase/document-based-application/frontend/components/ui/chart.tsx
new file mode 100644
index 0000000..8620baa
--- /dev/null
+++ b/usecase/document-based-application/frontend/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+