create-featurebased-architecture 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -9
- package/dist/index.js +4 -2
- package/package.json +2 -2
- package/templates/ollama-chatbot-backend/.env.example +9 -0
- package/templates/ollama-chatbot-backend/README.md +132 -0
- package/templates/ollama-chatbot-backend/package.json +21 -0
- package/templates/ollama-chatbot-backend/src/config/database.ts +4 -0
- package/templates/ollama-chatbot-backend/src/config/env.ts +8 -0
- package/templates/ollama-chatbot-backend/src/config/index.ts +3 -0
- package/templates/ollama-chatbot-backend/src/config/ollama.ts +14 -0
- package/templates/ollama-chatbot-backend/src/features/chat/controller.ts +49 -0
- package/templates/ollama-chatbot-backend/src/features/chat/index.ts +5 -0
- package/templates/ollama-chatbot-backend/src/features/chat/routes.ts +7 -0
- package/templates/ollama-chatbot-backend/src/features/chat/schema.ts +13 -0
- package/templates/ollama-chatbot-backend/src/features/chat/service.ts +91 -0
- package/templates/ollama-chatbot-backend/src/features/chat/types.ts +22 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/controller.ts +114 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/index.ts +6 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/repository.ts +61 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/routes.ts +11 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/schema.ts +9 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/service.ts +28 -0
- package/templates/ollama-chatbot-backend/src/features/conversations/types.ts +23 -0
- package/templates/ollama-chatbot-backend/src/index.ts +22 -0
- package/templates/ollama-chatbot-backend/src/routes/index.ts +10 -0
- package/templates/ollama-chatbot-backend/src/shared/index.ts +2 -0
- package/templates/ollama-chatbot-backend/src/shared/types/index.ts +16 -0
- package/templates/ollama-chatbot-backend/src/shared/utils/index.ts +1 -0
- package/templates/ollama-chatbot-backend/src/shared/utils/response.ts +10 -0
- package/templates/ollama-chatbot-backend/tsconfig.json +22 -0
- package/templates/ollama-chatbot-frontend/.env.example +1 -0
- package/templates/ollama-chatbot-frontend/README.md +65 -0
- package/templates/ollama-chatbot-frontend/index.html +12 -0
- package/templates/ollama-chatbot-frontend/package.json +23 -0
- package/templates/ollama-chatbot-frontend/src/App.tsx +17 -0
- package/templates/ollama-chatbot-frontend/src/config/env.ts +1 -0
- package/templates/ollama-chatbot-frontend/src/config/index.ts +1 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/components/ChatPage.tsx +94 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/components/index.ts +1 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/hooks/index.ts +1 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/hooks/useChat.ts +149 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/index.ts +4 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/services/chatService.ts +81 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/services/index.ts +1 -0
- package/templates/ollama-chatbot-frontend/src/features/chat/types.ts +33 -0
- package/templates/ollama-chatbot-frontend/src/index.css +281 -0
- package/templates/ollama-chatbot-frontend/src/main.tsx +13 -0
- package/templates/ollama-chatbot-frontend/src/shared/components/Sidebar.tsx +56 -0
- package/templates/ollama-chatbot-frontend/src/shared/components/index.ts +1 -0
- package/templates/ollama-chatbot-frontend/tsconfig.json +27 -0
- package/templates/ollama-chatbot-frontend/tsconfig.node.json +11 -0
- package/templates/ollama-chatbot-frontend/vite.config.ts +12 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { conversationController } from "./controller";
|
|
3
|
+
|
|
4
|
+
export const conversationRoutes = new Hono();
|
|
5
|
+
|
|
6
|
+
conversationRoutes.get("/", conversationController.getAll);
|
|
7
|
+
conversationRoutes.get("/:id", conversationController.getById);
|
|
8
|
+
conversationRoutes.get("/:id/messages", conversationController.getMessages);
|
|
9
|
+
conversationRoutes.post("/", conversationController.create);
|
|
10
|
+
conversationRoutes.put("/:id", conversationController.updateTitle);
|
|
11
|
+
conversationRoutes.delete("/:id", conversationController.delete);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const createConversationSchema = z.object({
|
|
4
|
+
title: z.string().min(1, "Title is required").max(200, "Title too long"),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const conversationIdSchema = z.object({
|
|
8
|
+
id: z.string().uuid("Invalid conversation ID"),
|
|
9
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { conversationRepository } from "./repository";
|
|
2
|
+
import type { Conversation, Message, CreateConversationDto } from "./types";
|
|
3
|
+
|
|
4
|
+
export const conversationService = {
|
|
5
|
+
async getAllConversations(): Promise<Conversation[]> {
|
|
6
|
+
return conversationRepository.findAll();
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
async getConversationById(id: string): Promise<Conversation | null> {
|
|
10
|
+
return conversationRepository.findById(id);
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async createConversation(data: CreateConversationDto): Promise<Conversation> {
|
|
14
|
+
return conversationRepository.create(data);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async updateConversationTitle(id: string, title: string): Promise<Conversation | null> {
|
|
18
|
+
return conversationRepository.updateTitle(id, title);
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async deleteConversation(id: string): Promise<boolean> {
|
|
22
|
+
return conversationRepository.delete(id);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async getConversationMessages(conversationId: string): Promise<Message[]> {
|
|
26
|
+
return conversationRepository.getMessages(conversationId);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Conversation {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
created_at: Date;
|
|
5
|
+
updated_at: Date;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Message {
|
|
9
|
+
id: string;
|
|
10
|
+
conversation_id: string;
|
|
11
|
+
role: "user" | "assistant" | "system";
|
|
12
|
+
content: string;
|
|
13
|
+
created_at: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreateConversationDto {
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AddMessageDto {
|
|
21
|
+
role: "user" | "assistant" | "system";
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { logger } from "hono/logger";
|
|
4
|
+
import { env } from "./config/env";
|
|
5
|
+
import { appRouter } from "./routes";
|
|
6
|
+
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
|
|
9
|
+
// Middleware
|
|
10
|
+
app.use("*", logger());
|
|
11
|
+
app.use("*", cors());
|
|
12
|
+
|
|
13
|
+
// Routes
|
|
14
|
+
app.route("/api", appRouter);
|
|
15
|
+
|
|
16
|
+
// Health check
|
|
17
|
+
app.get("/health", (c) => c.json({ status: "ok", timestamp: new Date().toISOString() }));
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
port: env.PORT,
|
|
21
|
+
fetch: app.fetch,
|
|
22
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { chatRoutes } from "../features/chat/routes";
|
|
3
|
+
import { conversationRoutes } from "../features/conversations/routes";
|
|
4
|
+
|
|
5
|
+
export const appRouter = new Hono();
|
|
6
|
+
|
|
7
|
+
appRouter.route("/chat", chatRoutes);
|
|
8
|
+
appRouter.route("/conversations", conversationRoutes);
|
|
9
|
+
|
|
10
|
+
appRouter.get("/", (c) => c.json({ message: "Ollama Chatbot API" }));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Shared types across features
|
|
2
|
+
export interface ApiResponse<T> {
|
|
3
|
+
success: boolean;
|
|
4
|
+
data?: T;
|
|
5
|
+
error?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
|
|
10
|
+
pagination: {
|
|
11
|
+
page: number;
|
|
12
|
+
limit: number;
|
|
13
|
+
total: number;
|
|
14
|
+
totalPages: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./response";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { ApiResponse } from "../types";
|
|
3
|
+
|
|
4
|
+
export function successResponse<T>(c: Context, data: T, message?: string, status = 200) {
|
|
5
|
+
return c.json<ApiResponse<T>>({ success: true, data, message }, status);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function errorResponse(c: Context, error: string, status = 400) {
|
|
9
|
+
return c.json<ApiResponse<never>>({ success: false, error }, status);
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"rootDir": "./src",
|
|
12
|
+
"baseUrl": ".",
|
|
13
|
+
"paths": {
|
|
14
|
+
"@/*": ["src/*"],
|
|
15
|
+
"@/features/*": ["src/features/*"],
|
|
16
|
+
"@/shared/*": ["src/shared/*"],
|
|
17
|
+
"@/config/*": ["src/config/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VITE_API_URL=http://localhost:3000/api
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
An Ollama Chatbot frontend with feature-based architecture using React, Vite, and react-icons.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── config/ # Configuration
|
|
10
|
+
├── features/
|
|
11
|
+
│ └── chat/
|
|
12
|
+
│ ├── components/
|
|
13
|
+
│ ├── hooks/
|
|
14
|
+
│ ├── services/
|
|
15
|
+
│ ├── types.ts
|
|
16
|
+
│ └── index.ts
|
|
17
|
+
├── shared/
|
|
18
|
+
│ └── components/
|
|
19
|
+
├── App.tsx
|
|
20
|
+
├── main.tsx
|
|
21
|
+
└── index.css
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Install dependencies
|
|
28
|
+
bun install
|
|
29
|
+
|
|
30
|
+
# Copy environment file
|
|
31
|
+
cp .env.example .env
|
|
32
|
+
|
|
33
|
+
# Run development server
|
|
34
|
+
bun run dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Environment Variables
|
|
38
|
+
|
|
39
|
+
| Variable | Description |
|
|
40
|
+
|----------|-------------|
|
|
41
|
+
| `VITE_API_URL` | Backend API URL (default: http://localhost:3000/api) |
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- Real-time chat with streaming responses
|
|
46
|
+
- Conversation history
|
|
47
|
+
- Create new conversations
|
|
48
|
+
- Delete conversations
|
|
49
|
+
- Dark theme UI
|
|
50
|
+
|
|
51
|
+
## Backend
|
|
52
|
+
|
|
53
|
+
This frontend requires the **Ollama Chatbot Backend** to be running. Generate both templates and run the backend first.
|
|
54
|
+
|
|
55
|
+
## Customization
|
|
56
|
+
|
|
57
|
+
### Changing the Model
|
|
58
|
+
|
|
59
|
+
The model is configured in the backend. Update the `.env` file in your backend project:
|
|
60
|
+
|
|
61
|
+
```env
|
|
62
|
+
OLLAMA_MODEL=llama3:70b-cloud
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or pass a specific model per request in the frontend by modifying the `sendMessage` call in `useChat.ts`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{PROJECT_NAME}} - Chatbot</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "bunx --bun vite",
|
|
7
|
+
"build": "bunx --bun vite build",
|
|
8
|
+
"preview": "bunx --bun vite preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"react": "^18.2.0",
|
|
12
|
+
"react-dom": "^18.2.0",
|
|
13
|
+
"react-router-dom": "^6.20.0",
|
|
14
|
+
"react-icons": "^5.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^18.2.0",
|
|
18
|
+
"@types/react-dom": "^18.2.0",
|
|
19
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"vite": "^5.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Routes, Route } from "react-router-dom";
|
|
2
|
+
import { ChatPage } from "./features/chat";
|
|
3
|
+
import { Sidebar } from "./shared/components";
|
|
4
|
+
|
|
5
|
+
export function App() {
|
|
6
|
+
return (
|
|
7
|
+
<div className="app-container">
|
|
8
|
+
<Sidebar />
|
|
9
|
+
<main className="main-content">
|
|
10
|
+
<Routes>
|
|
11
|
+
<Route path="/" element={<ChatPage />} />
|
|
12
|
+
<Route path="/chat/:conversationId" element={<ChatPage />} />
|
|
13
|
+
</Routes>
|
|
14
|
+
</main>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3000/api";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./env";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import { IoSend } from "react-icons/io5";
|
|
4
|
+
import { FaRobot, FaUser } from "react-icons/fa";
|
|
5
|
+
import { RiChatNewLine } from "react-icons/ri";
|
|
6
|
+
import { useChat } from "../hooks/useChat";
|
|
7
|
+
|
|
8
|
+
export function ChatPage() {
|
|
9
|
+
const { conversationId } = useParams();
|
|
10
|
+
const { messages, loading, streaming, sendMessage } = useChat(conversationId);
|
|
11
|
+
const [input, setInput] = useState("");
|
|
12
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
|
|
14
|
+
const scrollToBottom = () => {
|
|
15
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
scrollToBottom();
|
|
20
|
+
}, [messages]);
|
|
21
|
+
|
|
22
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
if (!input.trim() || streaming) return;
|
|
25
|
+
sendMessage(input);
|
|
26
|
+
setInput("");
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
30
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
handleSubmit(e);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if (loading && messages.length === 0) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="chat-container">
|
|
39
|
+
<div className="empty-state">
|
|
40
|
+
<p>Loading...</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div className="chat-container">
|
|
48
|
+
<div className="chat-messages">
|
|
49
|
+
{messages.length === 0 ? (
|
|
50
|
+
<div className="empty-state">
|
|
51
|
+
<RiChatNewLine className="empty-state-icon" />
|
|
52
|
+
<h2>Start a conversation</h2>
|
|
53
|
+
<p>Send a message to begin chatting with the AI assistant</p>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
messages.map((msg) => (
|
|
57
|
+
<div key={msg.id} className={`message ${msg.role}`}>
|
|
58
|
+
<div className="message-avatar">
|
|
59
|
+
{msg.role === "user" ? <FaUser size={16} /> : <FaRobot size={16} />}
|
|
60
|
+
</div>
|
|
61
|
+
<div className="message-content">
|
|
62
|
+
{msg.content || (
|
|
63
|
+
<div className="typing-indicator">
|
|
64
|
+
<span></span>
|
|
65
|
+
<span></span>
|
|
66
|
+
<span></span>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
))
|
|
72
|
+
)}
|
|
73
|
+
<div ref={messagesEndRef} />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="chat-input-container">
|
|
77
|
+
<form onSubmit={handleSubmit} className="chat-input-wrapper">
|
|
78
|
+
<textarea
|
|
79
|
+
className="chat-input"
|
|
80
|
+
value={input}
|
|
81
|
+
onChange={(e) => setInput(e.target.value)}
|
|
82
|
+
onKeyDown={handleKeyDown}
|
|
83
|
+
placeholder="Type your message..."
|
|
84
|
+
rows={1}
|
|
85
|
+
disabled={streaming}
|
|
86
|
+
/>
|
|
87
|
+
<button type="submit" className="send-btn" disabled={!input.trim() || streaming}>
|
|
88
|
+
<IoSend size={18} />
|
|
89
|
+
</button>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ChatPage";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./useChat";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { chatService } from "../services/chatService";
|
|
3
|
+
import type { Conversation, Message } from "../types";
|
|
4
|
+
|
|
5
|
+
export function useChat(conversationId?: string) {
|
|
6
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
7
|
+
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
8
|
+
const [currentConversationId, setCurrentConversationId] = useState<string | undefined>(conversationId);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
const [streaming, setStreaming] = useState(false);
|
|
11
|
+
|
|
12
|
+
// Load conversations
|
|
13
|
+
const loadConversations = useCallback(async () => {
|
|
14
|
+
try {
|
|
15
|
+
const data = await chatService.getConversations();
|
|
16
|
+
setConversations(data);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error("Failed to load conversations:", error);
|
|
19
|
+
}
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
// Load messages for a conversation
|
|
23
|
+
const loadMessages = useCallback(async (convId: string) => {
|
|
24
|
+
try {
|
|
25
|
+
setLoading(true);
|
|
26
|
+
const data = await chatService.getMessages(convId);
|
|
27
|
+
setMessages(data);
|
|
28
|
+
setCurrentConversationId(convId);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error("Failed to load messages:", error);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
// Send message
|
|
37
|
+
const sendMessage = useCallback(async (content: string, useStream = true) => {
|
|
38
|
+
if (!content.trim()) return;
|
|
39
|
+
|
|
40
|
+
// Add user message optimistically
|
|
41
|
+
const userMessage: Message = {
|
|
42
|
+
id: `temp-${Date.now()}`,
|
|
43
|
+
conversation_id: currentConversationId || "",
|
|
44
|
+
role: "user",
|
|
45
|
+
content,
|
|
46
|
+
created_at: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
49
|
+
|
|
50
|
+
if (useStream) {
|
|
51
|
+
setStreaming(true);
|
|
52
|
+
let assistantMessage: Message = {
|
|
53
|
+
id: `temp-assistant-${Date.now()}`,
|
|
54
|
+
conversation_id: currentConversationId || "",
|
|
55
|
+
role: "assistant",
|
|
56
|
+
content: "",
|
|
57
|
+
created_at: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
for await (const chunk of chatService.streamMessage({
|
|
63
|
+
message: content,
|
|
64
|
+
conversationId: currentConversationId,
|
|
65
|
+
})) {
|
|
66
|
+
if (chunk.conversationId && !currentConversationId) {
|
|
67
|
+
setCurrentConversationId(chunk.conversationId);
|
|
68
|
+
}
|
|
69
|
+
assistantMessage = { ...assistantMessage, content: assistantMessage.content + chunk.content };
|
|
70
|
+
setMessages((prev) => [...prev.slice(0, -1), assistantMessage]);
|
|
71
|
+
}
|
|
72
|
+
loadConversations();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Stream error:", error);
|
|
75
|
+
setMessages((prev) => prev.slice(0, -1));
|
|
76
|
+
} finally {
|
|
77
|
+
setStreaming(false);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
setLoading(true);
|
|
81
|
+
try {
|
|
82
|
+
const response = await chatService.sendMessage({
|
|
83
|
+
message: content,
|
|
84
|
+
conversationId: currentConversationId,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!currentConversationId) {
|
|
88
|
+
setCurrentConversationId(response.conversationId);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const assistantMessage: Message = {
|
|
92
|
+
id: `assistant-${Date.now()}`,
|
|
93
|
+
conversation_id: response.conversationId,
|
|
94
|
+
role: "assistant",
|
|
95
|
+
content: response.message,
|
|
96
|
+
created_at: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
99
|
+
loadConversations();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Send error:", error);
|
|
102
|
+
} finally {
|
|
103
|
+
setLoading(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [currentConversationId, loadConversations]);
|
|
107
|
+
|
|
108
|
+
// Delete conversation
|
|
109
|
+
const deleteConversation = useCallback(async (id: string) => {
|
|
110
|
+
try {
|
|
111
|
+
await chatService.deleteConversation(id);
|
|
112
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
113
|
+
if (id === currentConversationId) {
|
|
114
|
+
setMessages([]);
|
|
115
|
+
setCurrentConversationId(undefined);
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("Failed to delete:", error);
|
|
119
|
+
}
|
|
120
|
+
}, [currentConversationId]);
|
|
121
|
+
|
|
122
|
+
// New chat
|
|
123
|
+
const startNewChat = useCallback(() => {
|
|
124
|
+
setMessages([]);
|
|
125
|
+
setCurrentConversationId(undefined);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
loadConversations();
|
|
130
|
+
}, [loadConversations]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (conversationId) {
|
|
134
|
+
loadMessages(conversationId);
|
|
135
|
+
}
|
|
136
|
+
}, [conversationId, loadMessages]);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
messages,
|
|
140
|
+
conversations,
|
|
141
|
+
currentConversationId,
|
|
142
|
+
loading,
|
|
143
|
+
streaming,
|
|
144
|
+
sendMessage,
|
|
145
|
+
deleteConversation,
|
|
146
|
+
startNewChat,
|
|
147
|
+
loadMessages,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { API_URL } from "../../config/env";
|
|
2
|
+
import type { Conversation, Message, ChatRequest, ChatResponse, ApiResponse } from "./types";
|
|
3
|
+
|
|
4
|
+
export const chatService = {
|
|
5
|
+
// Conversations
|
|
6
|
+
async getConversations(): Promise<Conversation[]> {
|
|
7
|
+
const res = await fetch(`${API_URL}/conversations`);
|
|
8
|
+
const data: ApiResponse<Conversation[]> = await res.json();
|
|
9
|
+
if (!data.success) throw new Error(data.error);
|
|
10
|
+
return data.data || [];
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async getConversation(id: string): Promise<Conversation> {
|
|
14
|
+
const res = await fetch(`${API_URL}/conversations/${id}`);
|
|
15
|
+
const data: ApiResponse<Conversation> = await res.json();
|
|
16
|
+
if (!data.success) throw new Error(data.error);
|
|
17
|
+
return data.data!;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async getMessages(conversationId: string): Promise<Message[]> {
|
|
21
|
+
const res = await fetch(`${API_URL}/conversations/${conversationId}/messages`);
|
|
22
|
+
const data: ApiResponse<Message[]> = await res.json();
|
|
23
|
+
if (!data.success) throw new Error(data.error);
|
|
24
|
+
return data.data || [];
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async deleteConversation(id: string): Promise<void> {
|
|
28
|
+
const res = await fetch(`${API_URL}/conversations/${id}`, { method: "DELETE" });
|
|
29
|
+
const data: ApiResponse<null> = await res.json();
|
|
30
|
+
if (!data.success) throw new Error(data.error);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// Chat
|
|
34
|
+
async sendMessage(request: ChatRequest): Promise<ChatResponse> {
|
|
35
|
+
const res = await fetch(`${API_URL}/chat`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify(request),
|
|
39
|
+
});
|
|
40
|
+
const data: ApiResponse<ChatResponse> = await res.json();
|
|
41
|
+
if (!data.success) throw new Error(data.error);
|
|
42
|
+
return data.data!;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Stream chat
|
|
46
|
+
async *streamMessage(request: ChatRequest): AsyncGenerator<{ content: string; conversationId: string }> {
|
|
47
|
+
const res = await fetch(`${API_URL}/chat/stream`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
body: JSON.stringify(request),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok) throw new Error("Failed to stream");
|
|
54
|
+
|
|
55
|
+
const reader = res.body?.getReader();
|
|
56
|
+
if (!reader) throw new Error("No reader");
|
|
57
|
+
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let buffer = "";
|
|
60
|
+
|
|
61
|
+
while (true) {
|
|
62
|
+
const { done, value } = await reader.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
|
|
65
|
+
buffer += decoder.decode(value, { stream: true });
|
|
66
|
+
const lines = buffer.split("\n");
|
|
67
|
+
buffer = lines.pop() || "";
|
|
68
|
+
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith("data: ")) {
|
|
71
|
+
try {
|
|
72
|
+
const data = JSON.parse(line.slice(6));
|
|
73
|
+
yield data;
|
|
74
|
+
} catch {
|
|
75
|
+
// Skip invalid JSON
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./chatService";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface Conversation {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
created_at: string;
|
|
5
|
+
updated_at: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Message {
|
|
9
|
+
id: string;
|
|
10
|
+
conversation_id: string;
|
|
11
|
+
role: "user" | "assistant" | "system";
|
|
12
|
+
content: string;
|
|
13
|
+
created_at: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ChatRequest {
|
|
17
|
+
message: string;
|
|
18
|
+
conversationId?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatResponse {
|
|
23
|
+
message: string;
|
|
24
|
+
conversationId: string;
|
|
25
|
+
model: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ApiResponse<T> {
|
|
29
|
+
success: boolean;
|
|
30
|
+
data?: T;
|
|
31
|
+
error?: string;
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|