sam2ai commited on
Commit
0971cc4
1 Parent(s): 6e8dfa5

Synced repo using 'sync_with_huggingface' Github Action

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +9 -0
  2. .eslintrc.json +3 -0
  3. .example.env +1 -0
  4. .gitattributes +1 -0
  5. Dockerfile +50 -0
  6. LICENSE +21 -0
  7. Makefile +2 -0
  8. components.json +17 -0
  9. next.config.mjs +8 -0
  10. ollama-nextjs-ui.gif +3 -0
  11. package.json +49 -0
  12. postcss.config.js +6 -0
  13. public/next.svg +1 -0
  14. public/ollama.png +0 -0
  15. public/vercel.svg +1 -0
  16. src/app/api/chat/route.ts +222 -0
  17. src/app/api/health/route.ts +5 -0
  18. src/app/api/models/route.ts +55 -0
  19. src/app/chats/[id]/page.tsx +15 -0
  20. src/app/favicon.ico +0 -0
  21. src/app/globals.css +95 -0
  22. src/app/layout.tsx +36 -0
  23. src/app/page.tsx +9 -0
  24. src/components/chat/chat-bottombar.tsx +100 -0
  25. src/components/chat/chat-layout.tsx +83 -0
  26. src/components/chat/chat-list.tsx +136 -0
  27. src/components/chat/chat-options.ts +5 -0
  28. src/components/chat/chat-page.tsx +110 -0
  29. src/components/chat/chat-topbar.tsx +175 -0
  30. src/components/chat/chat.tsx +70 -0
  31. src/components/code-display-block.tsx +56 -0
  32. src/components/settings-clear-chats.tsx +70 -0
  33. src/components/settings-theme-toggle.tsx +36 -0
  34. src/components/settings.tsx +71 -0
  35. src/components/sidebar-skeleton.tsx +33 -0
  36. src/components/sidebar-tabs.tsx +167 -0
  37. src/components/sidebar.tsx +163 -0
  38. src/components/system-prompt.tsx +55 -0
  39. src/components/ui/button.tsx +59 -0
  40. src/components/ui/card.tsx +76 -0
  41. src/components/ui/dialog.tsx +115 -0
  42. src/components/ui/input.tsx +25 -0
  43. src/components/ui/sheet.tsx +138 -0
  44. src/components/ui/skeleton.tsx +15 -0
  45. src/components/ui/sonner.tsx +31 -0
  46. src/components/ui/tabs.tsx +31 -0
  47. src/components/ui/textarea.tsx +24 -0
  48. src/components/ui/tooltip.tsx +31 -0
  49. src/lib/mistral-tokenizer-js.d.ts +3 -0
  50. src/lib/token-counter.ts +21 -0
.dockerignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ .next/
2
+ node_modules/
3
+ .env
4
+ next-env.d.ts
5
+ Dockerfile
6
+ .dockerignore
7
+ Makefile
8
+ .github
9
+ .git
.eslintrc.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
.example.env ADDED
@@ -0,0 +1 @@
 
 
1
+ VLLM_URL="http://localhost:8000"
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ollama-nextjs-ui.gif filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ARG NODE_VERSION=lts
2
+ # Install dependencies only when needed
3
+ FROM node:$NODE_VERSION-alpine AS builder
4
+ ENV YARN_CACHE_FOLDER=/opt/yarncache
5
+ WORKDIR /opt/app
6
+ COPY package.json yarn.lock ./
7
+ RUN yarn install --frozen-lockfile
8
+ # patch logging for requestHandler
9
+ RUN sed -Ei \
10
+ -e '/await requestHandler/iconst __start = new Date;' \
11
+ -e '/await requestHandler/aconsole.log(`[${__start.toISOString()}] ${((new Date - __start) / 1000).toFixed(3)} ${req.method} ${req.url}`);' \
12
+ node_modules/next/dist/server/lib/start-server.js
13
+
14
+ ENV NODE_ENV=production
15
+ ENV NEXT_TELEMETRY_DISABLED=1
16
+ ENV VLLM_URL="http://localhost:8000"
17
+ ENV VLLM_API_KEY=""
18
+
19
+ COPY . .
20
+ RUN yarn build
21
+
22
+ # Production image, copy all the files and run next
23
+ FROM node:$NODE_VERSION-alpine AS runner
24
+ WORKDIR /opt/app
25
+
26
+ ENV NODE_ENV production
27
+ ENV NEXT_TELEMETRY_DISABLED 1
28
+
29
+ RUN addgroup --system --gid 1001 nodejs
30
+ RUN adduser --system --uid 1001 nextjs
31
+
32
+ COPY --from=builder /opt/app/public ./public
33
+
34
+ # Set the correct permission for prerender cache
35
+ RUN mkdir .next
36
+ RUN chown nextjs:nodejs .next
37
+
38
+ # Automatically leverage output traces to reduce image size
39
+ # https://nextjs.org/docs/advanced-features/output-file-tracing
40
+ COPY --from=builder --chown=nextjs:nodejs /opt/app/.next/standalone ./
41
+ COPY --from=builder --chown=nextjs:nodejs /opt/app/.next/static ./.next/static
42
+
43
+ USER nextjs
44
+
45
+ EXPOSE 3000
46
+
47
+ ENV PORT 3000
48
+ # set hostname to localhost
49
+ ENV HOSTNAME "0.0.0.0"
50
+ CMD ["node", "server.js"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jakob Hoeg Mørk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Makefile ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ build:
2
+ docker buildx build --platform linux/amd64,linux/arm64 . -t yoziru/nextjs-vllm-ui
components.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils"
16
+ }
17
+ }
next.config.mjs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ // set basepath based on environment
4
+ basePath: process.env.NEXT_PUBLIC_BASE_PATH ?? "",
5
+ output: "standalone",
6
+ };
7
+
8
+ export default nextConfig;
ollama-nextjs-ui.gif ADDED

Git LFS Details

  • SHA256: 91d7ea9d057e42d0ce46a3b0b1f550511d3b15581ce6d43bddf3728c28d70658
  • Pointer size: 132 Bytes
  • Size of remote file: 5.8 MB
package.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nextjs-vllm-ui",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@hookform/resolvers": "^3.3.4",
13
+ "@radix-ui/react-dialog": "^1.0.5",
14
+ "@radix-ui/react-icons": "^1.3.0",
15
+ "@radix-ui/react-slot": "^1.0.2",
16
+ "@radix-ui/react-tabs": "^1.0.4",
17
+ "@radix-ui/react-tooltip": "^1.0.7",
18
+ "ai": "^3.0.15",
19
+ "class-variance-authority": "^0.7.0",
20
+ "clsx": "^2.1.0",
21
+ "mistral-tokenizer-js": "^1.0.0",
22
+ "next": "14.1.4",
23
+ "next-themes": "^0.3.0",
24
+ "openai": "^4.30.0",
25
+ "react": "^18",
26
+ "react-code-blocks": "^0.1.6",
27
+ "react-dom": "^18",
28
+ "react-hook-form": "^7.51.2",
29
+ "react-textarea-autosize": "^8.5.3",
30
+ "sonner": "^1.4.41",
31
+ "tailwind-merge": "^2.2.2",
32
+ "use-debounce": "^10.0.0",
33
+ "use-local-storage-state": "^19.2.0",
34
+ "uuid": "^9.0.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.11.30",
38
+ "@types/react": "^18.2.73",
39
+ "@types/react-dom": "^18.2.23",
40
+ "@types/uuid": "^9.0.8",
41
+ "autoprefixer": "^10.4.19",
42
+ "eslint": "^8.57.0",
43
+ "eslint-config-next": "14.1.4",
44
+ "postcss": "^8.4.38",
45
+ "tailwindcss": "^3.4.3",
46
+ "typescript": "^5.4.3",
47
+ "typescript-eslint": "^7.4.0"
48
+ }
49
+ }
postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
public/next.svg ADDED
public/ollama.png ADDED
public/vercel.svg ADDED
src/app/api/chat/route.ts ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ createParser,
3
+ ParsedEvent,
4
+ ReconnectInterval,
5
+ } from "eventsource-parser";
6
+ import { NextRequest, NextResponse } from "next/server";
7
+ import {
8
+ ChatCompletionAssistantMessageParam,
9
+ ChatCompletionCreateParamsStreaming,
10
+ ChatCompletionMessageParam,
11
+ ChatCompletionSystemMessageParam,
12
+ ChatCompletionUserMessageParam,
13
+ } from "openai/resources/index.mjs";
14
+
15
+ import { encodeChat, tokenLimit } from "@/lib/token-counter";
16
+
17
+ const addSystemMessage = (
18
+ messages: ChatCompletionMessageParam[],
19
+ systemPrompt?: string
20
+ ) => {
21
+ // early exit if system prompt is empty
22
+ if (!systemPrompt || systemPrompt === "") {
23
+ return messages;
24
+ }
25
+
26
+ // add system prompt to the chat (if it's not already there)
27
+ // check first message in the chat
28
+ if (!messages) {
29
+ // if there are no messages, add the system prompt as the first message
30
+ messages = [
31
+ {
32
+ content: systemPrompt,
33
+ role: "system",
34
+ },
35
+ ];
36
+ } else if (messages.length === 0) {
37
+ // if there are no messages, add the system prompt as the first message
38
+ messages.push({
39
+ content: systemPrompt,
40
+ role: "system",
41
+ });
42
+ } else {
43
+ // if there are messages, check if the first message is a system prompt
44
+ if (messages[0].role === "system") {
45
+ // if the first message is a system prompt, update it
46
+ messages[0].content = systemPrompt;
47
+ } else {
48
+ // if the first message is not a system prompt, add the system prompt as the first message
49
+ messages.unshift({
50
+ content: systemPrompt,
51
+ role: "system",
52
+ });
53
+ }
54
+ }
55
+ return messages;
56
+ };
57
+
58
+ const formatMessages = (
59
+ messages: ChatCompletionMessageParam[]
60
+ ): ChatCompletionMessageParam[] => {
61
+ let mappedMessages: ChatCompletionMessageParam[] = [];
62
+ let messagesTokenCounts: number[] = [];
63
+ const responseTokens = 512;
64
+ const tokenLimitRemaining = tokenLimit - responseTokens;
65
+ let tokenCount = 0;
66
+
67
+ messages.forEach((m) => {
68
+ if (m.role === "system") {
69
+ mappedMessages.push({
70
+ role: "system",
71
+ content: m.content,
72
+ } as ChatCompletionSystemMessageParam);
73
+ } else if (m.role === "user") {
74
+ mappedMessages.push({
75
+ role: "user",
76
+ content: m.content,
77
+ } as ChatCompletionUserMessageParam);
78
+ } else if (m.role === "assistant") {
79
+ mappedMessages.push({
80
+ role: "assistant",
81
+ content: m.content,
82
+ } as ChatCompletionAssistantMessageParam);
83
+ } else {
84
+ return;
85
+ }
86
+
87
+ // ignore typing
88
+ // tslint:disable-next-line
89
+ const messageTokens = encodeChat([m]);
90
+ messagesTokenCounts.push(messageTokens);
91
+ tokenCount += messageTokens;
92
+ });
93
+
94
+ if (tokenCount <= tokenLimitRemaining) {
95
+ return mappedMessages;
96
+ }
97
+
98
+ // remove the middle messages until the token count is below the limit
99
+ while (tokenCount > tokenLimitRemaining) {
100
+ const middleMessageIndex = Math.floor(messages.length / 2);
101
+ const middleMessageTokens = messagesTokenCounts[middleMessageIndex];
102
+ mappedMessages.splice(middleMessageIndex, 1);
103
+ messagesTokenCounts.splice(middleMessageIndex, 1);
104
+ tokenCount -= middleMessageTokens;
105
+ }
106
+ return mappedMessages;
107
+ };
108
+
109
+ export async function POST(req: NextRequest): Promise<NextResponse> {
110
+ try {
111
+ const { messages, chatOptions } = await req.json();
112
+ if (!chatOptions.selectedModel || chatOptions.selectedModel === "") {
113
+ throw new Error("Selected model is required");
114
+ }
115
+
116
+ const baseUrl = process.env.VLLM_URL;
117
+ if (!baseUrl) {
118
+ throw new Error("VLLM_URL is not set");
119
+ }
120
+ const apiKey = process.env.VLLM_API_KEY;
121
+
122
+ const formattedMessages = formatMessages(
123
+ addSystemMessage(messages, chatOptions.systemPrompt)
124
+ );
125
+
126
+ const stream = await getOpenAIStream(
127
+ baseUrl,
128
+ chatOptions.selectedModel,
129
+ formattedMessages,
130
+ chatOptions.temperature,
131
+ apiKey,
132
+ );
133
+ return new NextResponse(stream, {
134
+ headers: { "Content-Type": "text/event-stream" },
135
+ });
136
+ } catch (error) {
137
+ console.error(error);
138
+ return NextResponse.json(
139
+ {
140
+ success: false,
141
+ error: error instanceof Error ? error.message : "Unknown error",
142
+ },
143
+ { status: 500 }
144
+ );
145
+ }
146
+ }
147
+
148
+ const getOpenAIStream = async (
149
+ apiUrl: string,
150
+ model: string,
151
+ messages: ChatCompletionMessageParam[],
152
+ temperature?: number,
153
+ apiKey?: string
154
+ ): Promise<ReadableStream<Uint8Array>> => {
155
+ const encoder = new TextEncoder();
156
+ const decoder = new TextDecoder();
157
+ const headers = new Headers();
158
+ headers.set("Content-Type", "application/json");
159
+ if (apiKey !== undefined) {
160
+ headers.set("Authorization", `Bearer ${apiKey}`);
161
+ headers.set("api-key", apiKey);
162
+ }
163
+ const chatOptions: ChatCompletionCreateParamsStreaming = {
164
+ model: model,
165
+ // frequency_penalty: 0,
166
+ // max_tokens: 2000,
167
+ messages: messages,
168
+ // presence_penalty: 0,
169
+ stream: true,
170
+ temperature: temperature ?? 0.5,
171
+ // response_format: {
172
+ // type: "json_object",
173
+ // }
174
+ // top_p: 0.95,
175
+ };
176
+ const res = await fetch(apiUrl + "/v1/chat/completions", {
177
+ headers: headers,
178
+ method: "POST",
179
+ body: JSON.stringify(chatOptions),
180
+ });
181
+
182
+ if (res.status !== 200) {
183
+ const statusText = res.statusText;
184
+ const responseBody = await res.text();
185
+ console.error(`vLLM API response error: ${responseBody}`);
186
+ throw new Error(
187
+ `The vLLM API has encountered an error with a status code of ${res.status} ${statusText}: ${responseBody}`
188
+ );
189
+ }
190
+
191
+ return new ReadableStream({
192
+ async start(controller) {
193
+ const onParse = (event: ParsedEvent | ReconnectInterval) => {
194
+ if (event.type === "event") {
195
+ const data = event.data;
196
+
197
+ if (data === "[DONE]") {
198
+ controller.close();
199
+ return;
200
+ }
201
+
202
+ try {
203
+ const json = JSON.parse(data);
204
+ const text = json.choices[0].delta.content;
205
+ const queue = encoder.encode(text);
206
+ controller.enqueue(queue);
207
+ } catch (e) {
208
+ controller.error(e);
209
+ }
210
+ }
211
+ };
212
+
213
+ const parser = createParser(onParse);
214
+
215
+ for await (const chunk of res.body as any) {
216
+ // An extra newline is required to make AzureOpenAI work.
217
+ const str = decoder.decode(chunk).replace("[DONE]\n", "[DONE]\n\n");
218
+ parser.feed(str);
219
+ }
220
+ },
221
+ });
222
+ };
src/app/api/health/route.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function GET(req: NextRequest): Promise<NextResponse> {
4
+ return new NextResponse("OK", { status: 200 });
5
+ }
src/app/api/models/route.ts ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export async function GET(req: NextRequest): Promise<NextResponse> {
4
+ const baseUrl = process.env.VLLM_URL;
5
+ const apiKey = process.env.VLLM_API_KEY;
6
+ const headers = new Headers();
7
+ if (apiKey !== undefined) {
8
+ headers.set("Authorization", `Bearer ${apiKey}`);
9
+ headers.set("api-key", apiKey);
10
+ }
11
+ if (!baseUrl) {
12
+ throw new Error("VLLM_URL is not set");
13
+ }
14
+
15
+ const envModel = process.env.VLLM_MODEL;
16
+ if (envModel) {
17
+ return NextResponse.json({
18
+ object: "list",
19
+ data: [
20
+ {
21
+ id: envModel,
22
+ },
23
+ ],
24
+ });
25
+ }
26
+
27
+ try {
28
+ const res = await fetch(`${baseUrl}/v1/models`, {
29
+ headers: headers,
30
+ cache: "no-store",
31
+ });
32
+ if (res.status !== 200) {
33
+ const statusText = res.statusText;
34
+ const responseBody = await res.text();
35
+ console.error(`vLLM /api/models response error: ${responseBody}`);
36
+ return NextResponse.json(
37
+ {
38
+ success: false,
39
+ error: statusText,
40
+ },
41
+ { status: res.status }
42
+ );
43
+ }
44
+ return new NextResponse(res.body, res);
45
+ } catch (error) {
46
+ console.error(error);
47
+ return NextResponse.json(
48
+ {
49
+ success: false,
50
+ error: error instanceof Error ? error.message : "Unknown error",
51
+ },
52
+ { status: 500 }
53
+ );
54
+ }
55
+ }
src/app/chats/[id]/page.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import ChatPage from "@/components/chat/chat-page";
6
+
7
+ export default function Page({ params }: { params: { id: string } }) {
8
+ const [chatId, setChatId] = React.useState<string>("");
9
+ React.useEffect(() => {
10
+ if (params.id) {
11
+ setChatId(params.id);
12
+ }
13
+ }, [params.id]);
14
+ return <ChatPage chatId={chatId} setChatId={setChatId} />;
15
+ }
src/app/favicon.ico ADDED
src/app/globals.css ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+
6
+ @layer base {
7
+ :root {
8
+ --background: 0 0% 100%;
9
+ --foreground: 240 10% 3.9%;
10
+ --card: 0 0% 100%;
11
+ --card-foreground: 240 10% 3.9%;
12
+ --popover: 0 0% 100%;
13
+ --popover-foreground: 240 10% 3.9%;
14
+ --primary: 240 5.9% 10%;
15
+ --primary-foreground: 0 0% 98%;
16
+ --secondary: 240 4.8% 95.9%;
17
+ --secondary-foreground: 240 5.9% 10%;
18
+ --muted: 240 4.8% 95.9%;
19
+ --muted-foreground: 240 3.8% 46.1%;
20
+ --accent: 240 4.8% 95.9%;
21
+ --accent-foreground: 240 5.9% 10%;
22
+ --destructive: 0 84.2% 60.2%;
23
+ --destructive-foreground: 0 0% 98%;
24
+ --border: 240 5.9% 90%;
25
+ --input: 240 5.9% 90%;
26
+ --ring: 240 5.9% 10%;
27
+ --radius: 1rem;
28
+ }
29
+
30
+ .dark {
31
+ --background: 0 0% 9%;
32
+ --foreground: 0 0% 98%;
33
+ --card: 0 0% 12%;
34
+ --card-foreground: 0 0% 98%;
35
+ --popover: 0 0% 12%;
36
+ --popover-foreground: 0 0% 98%;
37
+ --primary: 0 0% 98%;
38
+ --primary-foreground: 240 5.9% 10%;
39
+ --secondary: 240 3.7% 15.9%;
40
+ --secondary-foreground: 0 0% 98%;
41
+ --muted: 240 3.7% 15.9%;
42
+ --muted-foreground: 240 5% 64.9%;
43
+ --accent: 240 3.7% 15.9%;
44
+ --accent-foreground: 0 0% 98%;
45
+ --destructive: 0 62.8% 30.6%;
46
+ --destructive-foreground: 0 0% 98%;
47
+ --border: 240 3.7% 15.9%;
48
+ --input: 240 3.7% 15.9%;
49
+ --ring: 240 4.9% 83.9%;
50
+ }
51
+ }
52
+
53
+ @layer base {
54
+ * {
55
+ @apply border-border;
56
+ }
57
+ body {
58
+ @apply bg-background text-foreground;
59
+ }
60
+ }
61
+
62
+ #scroller * {
63
+ overflow-anchor: none;
64
+ }
65
+
66
+ #anchor {
67
+ overflow-anchor: auto;
68
+ height: 1px;
69
+ }
70
+
71
+ :root {
72
+ --scrollbar-thumb-color: #ccc;
73
+ --scrollbar-thumb-hover-color: #aaa;
74
+ }
75
+
76
+ ::-webkit-scrollbar {
77
+ width: 6px;
78
+ height: 6px;
79
+ }
80
+
81
+ ::-webkit-scrollbar-thumb {
82
+ background-color: var(--scrollbar-thumb-color);
83
+ border-radius: 999px;
84
+ transition: width 0.3s, height 0.3s, visibility 0.3s;
85
+ }
86
+
87
+ ::-webkit-scrollbar-thumb:hover {
88
+ background-color: var(--scrollbar-thumb-hover-color);
89
+ }
90
+
91
+ ::-webkit-scrollbar-thumb:not(:hover) {
92
+ width: 0;
93
+ height: 0;
94
+ visibility: hidden;
95
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ThemeProvider } from "@/providers/theme-provider";
2
+ import type { Metadata } from "next";
3
+
4
+ import { Toaster } from "@/components/ui/sonner";
5
+ import "./globals.css";
6
+
7
+ export const runtime = "edge"; // 'nodejs' (default) | 'edge'
8
+
9
+ export const metadata: Metadata = {
10
+ title: "vLLM UI",
11
+ description: "vLLM chatbot web interface",
12
+ };
13
+
14
+ export const viewport = {
15
+ width: "device-width",
16
+ initialScale: 1,
17
+ maximumScale: 1,
18
+ userScalable: 1,
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html lang="en">
28
+ <body>
29
+ <ThemeProvider attribute="class" defaultTheme="dark">
30
+ {children}
31
+ <Toaster />
32
+ </ThemeProvider>
33
+ </body>
34
+ </html>
35
+ );
36
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import ChatPage from "@/components/chat/chat-page";
5
+
6
+ export default function Home() {
7
+ const [chatId, setChatId] = React.useState<string>("");
8
+ return <ChatPage chatId={chatId} setChatId={setChatId} />;
9
+ }
src/components/chat/chat-bottombar.tsx ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { PaperPlaneIcon, StopIcon } from "@radix-ui/react-icons";
6
+ import { ChatRequestOptions } from "ai";
7
+ import mistralTokenizer from "mistral-tokenizer-js";
8
+ import TextareaAutosize from "react-textarea-autosize";
9
+
10
+ import { tokenLimit } from "@/lib/token-counter";
11
+ import { Button } from "../ui/button";
12
+
13
+ interface ChatBottombarProps {
14
+ selectedModel: string | undefined;
15
+ input: string;
16
+ handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
17
+ handleSubmit: (
18
+ e: React.FormEvent<HTMLFormElement>,
19
+ chatRequestOptions?: ChatRequestOptions
20
+ ) => void;
21
+ isLoading: boolean;
22
+ stop: () => void;
23
+ }
24
+
25
+ export default function ChatBottombar({
26
+ selectedModel,
27
+ input,
28
+ handleInputChange,
29
+ handleSubmit,
30
+ isLoading,
31
+ stop,
32
+ }: ChatBottombarProps) {
33
+ const inputRef = React.useRef<HTMLTextAreaElement>(null);
34
+ const hasSelectedModel = selectedModel && selectedModel !== "";
35
+
36
+ const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
37
+ if (e.key === "Enter" && !e.shiftKey && hasSelectedModel && !isLoading) {
38
+ e.preventDefault();
39
+ handleSubmit(e as unknown as React.FormEvent<HTMLFormElement>);
40
+ }
41
+ };
42
+ const tokenCount = input ? mistralTokenizer.encode(input).length - 1 : 0;
43
+
44
+ return (
45
+ <div>
46
+ <div className="stretch flex flex-row gap-3 last:mb-2 md:last:mb-6 mx-2 md:mx-4 md:mx-auto md:max-w-2xl xl:max-w-3xl">
47
+ {/* <div className="p-2 pb-1 flex justify-between w-full items-center "> */}
48
+ <div key="input" className="w-full relative mb-1 items-center">
49
+ <form
50
+ onSubmit={handleSubmit}
51
+ className="w-full items-center flex relative gap-2"
52
+ >
53
+ <TextareaAutosize
54
+ autoComplete="off"
55
+ value={input}
56
+ ref={inputRef}
57
+ onKeyDown={handleKeyPress}
58
+ onChange={handleInputChange}
59
+ name="message"
60
+ placeholder="Ask vLLM anything..."
61
+ className="border-input max-h-48 px-4 py-4 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 dark:focus-visible:ring-slate-500 disabled:cursor-not-allowed disabled:opacity-50 w-full border rounded-md flex items-center h-14 resize-none overflow-hidden dark:bg-card/35 pr-32"
62
+ />
63
+ <div className="text-xs text-muted-foreground absolute right-14 px-0 text-right">
64
+ {tokenCount > tokenLimit ? (
65
+ <span className="text-red-700">
66
+ {tokenCount} token{tokenCount == 1 ? "" : "s"}
67
+ </span>
68
+ ) : (
69
+ <span>
70
+ {tokenCount} token{tokenCount == 1 ? "" : "s"}
71
+ </span>
72
+ )}
73
+ </div>
74
+ {!isLoading ? (
75
+ <Button
76
+ size="icon"
77
+ className="absolute bottom-1.5 md:bottom-2 md:right-2 right-2 z-100"
78
+ type="submit"
79
+ disabled={isLoading || !input.trim() || !hasSelectedModel}
80
+ >
81
+ <PaperPlaneIcon className="w-5 h-5 text-white dark:text-black" />
82
+ </Button>
83
+ ) : (
84
+ <Button
85
+ size="icon"
86
+ className="absolute bottom-1.5 md:bottom-2 md:right-2 right-2 z-100"
87
+ onClick={stop}
88
+ >
89
+ <StopIcon className="w-5 h-5 text-white dark:text-black" />
90
+ </Button>
91
+ )}
92
+ </form>
93
+ </div>
94
+ </div>
95
+ <div className="relative px-2 py-2 text-center text-xs text-slate-500 md:px-[60px]">
96
+ <span>Enter to send, Shift + Enter for new line</span>
97
+ </div>
98
+ </div>
99
+ );
100
+ }
src/components/chat/chat-layout.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect, useState } from "react";
4
+
5
+ import { Sidebar } from "../sidebar";
6
+ import Chat, { ChatProps, ChatTopbarProps } from "./chat";
7
+
8
+ interface ChatLayoutProps {
9
+ defaultLayout: number[] | undefined;
10
+ defaultCollapsed?: boolean;
11
+ navCollapsedSize: number;
12
+ chatId: string;
13
+ }
14
+
15
+ type MergedProps = ChatLayoutProps & ChatProps & ChatTopbarProps;
16
+
17
+ export function ChatLayout({
18
+ defaultLayout = [30, 160],
19
+ defaultCollapsed = false,
20
+ navCollapsedSize = 768,
21
+ messages,
22
+ input,
23
+ handleInputChange,
24
+ handleSubmit,
25
+ isLoading,
26
+ error,
27
+ stop,
28
+ chatId,
29
+ setChatId,
30
+ chatOptions,
31
+ setChatOptions,
32
+ }: MergedProps) {
33
+ const [isCollapsed, setIsCollapsed] = React.useState(false);
34
+ const [isMobile, setIsMobile] = useState(false);
35
+
36
+ useEffect(() => {
37
+ const checkScreenWidth = () => {
38
+ setIsMobile(window.innerWidth <= 768);
39
+ setIsCollapsed(window.innerWidth <= 768);
40
+ };
41
+
42
+ // Initial check
43
+ checkScreenWidth();
44
+
45
+ // Event listener for screen width changes
46
+ window.addEventListener("resize", checkScreenWidth);
47
+
48
+ // Cleanup the event listener on component unmount
49
+ return () => {
50
+ window.removeEventListener("resize", checkScreenWidth);
51
+ };
52
+ }, []);
53
+
54
+ return (
55
+ <div className="relative z-0 flex h-full w-full overflow-hidden">
56
+ <div className="flex-shrink-0 overflow-x-hidden bg-token-sidebar-surface-primary md:w-[260px]">
57
+ <Sidebar
58
+ isCollapsed={isCollapsed}
59
+ isMobile={isMobile}
60
+ chatId={chatId}
61
+ setChatId={setChatId}
62
+ chatOptions={chatOptions}
63
+ setChatOptions={setChatOptions}
64
+ />
65
+ </div>
66
+ <div className="relative flex h-full max-w-full flex-1 flex-col overflow-hidden">
67
+ <Chat
68
+ chatId={chatId}
69
+ setChatId={setChatId}
70
+ chatOptions={chatOptions}
71
+ setChatOptions={setChatOptions}
72
+ messages={messages}
73
+ input={input}
74
+ handleInputChange={handleInputChange}
75
+ handleSubmit={handleSubmit}
76
+ isLoading={isLoading}
77
+ error={error}
78
+ stop={stop}
79
+ />
80
+ </div>
81
+ </div>
82
+ );
83
+ }
src/components/chat/chat-list.tsx ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ import Image from "next/image";
4
+
5
+ import OllamaLogo from "../../../public/ollama.png";
6
+ import CodeDisplayBlock from "../code-display-block";
7
+ import { Message } from "ai";
8
+
9
+ interface ChatListProps {
10
+ messages: Message[];
11
+ isLoading: boolean;
12
+ }
13
+
14
+ const MessageToolbar = () => (
15
+ <div className="mt-1 flex gap-3 empty:hidden juice:flex-row-reverse">
16
+ <div className="text-gray-400 flex self-end lg:self-center items-center justify-center lg:justify-start mt-0 -ml-1 h-7 gap-[2px] invisible">
17
+ <span>Regenerate</span>
18
+ <span>Edit</span>
19
+ </div>
20
+ </div>
21
+ );
22
+
23
+ export default function ChatList({ messages, isLoading }: ChatListProps) {
24
+ const bottomRef = useRef<HTMLDivElement>(null);
25
+
26
+ const scrollToBottom = () => {
27
+ bottomRef.current?.scrollIntoView({ behavior: "auto", block: "end" });
28
+ };
29
+
30
+ useEffect(() => {
31
+ if (isLoading) {
32
+ return;
33
+ }
34
+ // if user scrolls up, disable auto-scroll
35
+ scrollToBottom();
36
+ }, [messages, isLoading]);
37
+
38
+ if (messages.length === 0) {
39
+ return (
40
+ <div className="w-full h-full flex justify-center items-center">
41
+ <div className="flex flex-col gap-4 items-center">
42
+ <Image
43
+ src={OllamaLogo}
44
+ alt="AI"
45
+ width={60}
46
+ height={60}
47
+ className="h-20 w-14 object-contain dark:invert"
48
+ />
49
+ <p className="text-center text-xl text-muted-foreground">
50
+ How can I help you today?
51
+ </p>
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <div
59
+ id="scroller"
60
+ className="w-[800px] overflow-y-scroll overflow-x-hidden h-full justify-center m-auto"
61
+ >
62
+ <div className="px-4 py-2 justify-center text-base md:gap-6 m-auto">
63
+ {messages
64
+ .filter((message) => message.role !== "system")
65
+ .map((message, index) => (
66
+ <div
67
+ key={index}
68
+ className="flex flex-1 text-base mx-auto gap-3 juice:gap-4 juice:md:gap-6 md:px-5 lg:px-1 xl:px-5 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem]"
69
+ >
70
+ <div className="flex flex-1 text-base mx-auto gap-3 juice:gap-4 juice:md:gap-6 md:px-5 lg:px-1 xl:px-5 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem]">
71
+ <div className="flex-shrink-0 flex flex-col relative items-end">
72
+ <div className="pt-0.5">
73
+ <div className="gizmo-shadow-stroke flex h-6 w-6 items-center justify-center overflow-hidden rounded-full">
74
+ {message.role === "user" ? (
75
+ <div className="dark:invert h-full w-full bg-black" />
76
+ ) : (
77
+ <Image
78
+ src={OllamaLogo}
79
+ alt="AI"
80
+ className="object-contain dark:invert aspect-square h-full w-full"
81
+ />
82
+ )}
83
+ </div>
84
+ </div>
85
+ </div>
86
+ {message.role === "user" && (
87
+ <div className="relative flex w-full min-w-0 flex-col">
88
+ <div className="font-semibold pb-2">You</div>
89
+ <div className="flex-col gap-1 md:gap-3">
90
+ {message.content}
91
+ </div>
92
+ <MessageToolbar />
93
+ </div>
94
+ )}
95
+ {message.role === "assistant" && (
96
+ <div className="relative flex w-full min-w-0 flex-col">
97
+ <div className="font-semibold pb-2">Assistant</div>
98
+ <div className="flex-col gap-1 md:gap-3">
99
+ <span className="whitespace-pre-wrap">
100
+ {/* Check if the message content contains a code block */}
101
+ {message.content.split("```").map((part, index) => {
102
+ if (index % 2 === 0) {
103
+ return (
104
+ <React.Fragment key={index}>
105
+ {part}
106
+ </React.Fragment>
107
+ );
108
+ } else {
109
+ return (
110
+ <CodeDisplayBlock
111
+ key={index}
112
+ code={part.trim()}
113
+ lang=""
114
+ />
115
+ );
116
+ }
117
+ })}
118
+ {isLoading &&
119
+ messages.indexOf(message) === messages.length - 1 && (
120
+ <span className="animate-pulse" aria-label="Typing">
121
+ ...
122
+ </span>
123
+ )}
124
+ </span>
125
+ </div>
126
+ <MessageToolbar />
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ ))}
132
+ </div>
133
+ <div id="anchor" ref={bottomRef}></div>
134
+ </div>
135
+ );
136
+ }
src/components/chat/chat-options.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export interface ChatOptions {
2
+ selectedModel?: string;
3
+ systemPrompt?: string;
4
+ temperature?: number;
5
+ }
src/components/chat/chat-page.tsx ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { ChatRequestOptions } from "ai";
6
+ import { useChat } from "ai/react";
7
+ import { toast } from "sonner";
8
+ import useLocalStorageState from "use-local-storage-state";
9
+ import { v4 as uuidv4 } from "uuid";
10
+
11
+ import { ChatLayout } from "@/components/chat/chat-layout";
12
+ import { ChatOptions } from "@/components/chat/chat-options";
13
+ import { basePath } from "@/lib/utils";
14
+
15
+ interface ChatPageProps {
16
+ chatId: string;
17
+ setChatId: React.Dispatch<React.SetStateAction<string>>;
18
+ }
19
+ export default function ChatPage({ chatId, setChatId }: ChatPageProps) {
20
+ const {
21
+ messages,
22
+ input,
23
+ handleInputChange,
24
+ handleSubmit,
25
+ isLoading,
26
+ error,
27
+ stop,
28
+ setMessages,
29
+ } = useChat({
30
+ api: basePath + "/api/chat",
31
+ streamMode: "text",
32
+ onError: (error) => {
33
+ toast.error("Something went wrong: " + error);
34
+ },
35
+ });
36
+ const [chatOptions, setChatOptions] = useLocalStorageState<ChatOptions>(
37
+ "chatOptions",
38
+ {
39
+ defaultValue: {
40
+ selectedModel: "",
41
+ systemPrompt: "",
42
+ temperature: 0.9,
43
+ },
44
+ }
45
+ );
46
+
47
+ React.useEffect(() => {
48
+ if (chatId) {
49
+ const item = localStorage.getItem(`chat_${chatId}`);
50
+ if (item) {
51
+ setMessages(JSON.parse(item));
52
+ }
53
+ } else {
54
+ setMessages([]);
55
+ }
56
+ }, [setMessages, chatId]);
57
+
58
+ React.useEffect(() => {
59
+ if (!isLoading && !error && chatId && messages.length > 0) {
60
+ // Save messages to local storage
61
+ localStorage.setItem(`chat_${chatId}`, JSON.stringify(messages));
62
+ // Trigger the storage event to update the sidebar component
63
+ window.dispatchEvent(new Event("storage"));
64
+ }
65
+ }, [messages, chatId, isLoading, error]);
66
+
67
+ const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
68
+ e.preventDefault();
69
+
70
+ if (messages.length === 0) {
71
+ // Generate a random id for the chat
72
+ const id = uuidv4();
73
+ setChatId(id);
74
+ }
75
+
76
+ setMessages([...messages]);
77
+
78
+ // Prepare the options object with additional body data, to pass the model.
79
+ const requestOptions: ChatRequestOptions = {
80
+ options: {
81
+ body: {
82
+ chatOptions: chatOptions,
83
+ },
84
+ },
85
+ };
86
+
87
+ // Call the handleSubmit function with the options
88
+ handleSubmit(e, requestOptions);
89
+ };
90
+
91
+ return (
92
+ <main className="flex h-[calc(100dvh)] flex-col items-center ">
93
+ <ChatLayout
94
+ chatId={chatId}
95
+ setChatId={setChatId}
96
+ chatOptions={chatOptions}
97
+ setChatOptions={setChatOptions}
98
+ messages={messages}
99
+ input={input}
100
+ handleInputChange={handleInputChange}
101
+ handleSubmit={onSubmit}
102
+ isLoading={isLoading}
103
+ error={error}
104
+ stop={stop}
105
+ navCollapsedSize={10}
106
+ defaultLayout={[30, 160]}
107
+ />
108
+ </main>
109
+ );
110
+ }
src/components/chat/chat-topbar.tsx ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useEffect } from "react";
4
+
5
+ import {
6
+ CheckCircledIcon,
7
+ CrossCircledIcon,
8
+ DotFilledIcon,
9
+ HamburgerMenuIcon,
10
+ InfoCircledIcon,
11
+ } from "@radix-ui/react-icons";
12
+ import { Message } from "ai/react";
13
+ import { toast } from "sonner";
14
+
15
+ import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
16
+ import {
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipProvider,
20
+ TooltipTrigger,
21
+ } from "@/components/ui/tooltip";
22
+ import { encodeChat, tokenLimit } from "@/lib/token-counter";
23
+ import { basePath, useHasMounted } from "@/lib/utils";
24
+ import { Sidebar } from "../sidebar";
25
+ import { ChatOptions } from "./chat-options";
26
+
27
+ interface ChatTopbarProps {
28
+ chatOptions: ChatOptions;
29
+ setChatOptions: React.Dispatch<React.SetStateAction<ChatOptions>>;
30
+ isLoading: boolean;
31
+ chatId?: string;
32
+ setChatId: React.Dispatch<React.SetStateAction<string>>;
33
+ messages: Message[];
34
+ }
35
+
36
+ export default function ChatTopbar({
37
+ chatOptions,
38
+ setChatOptions,
39
+ isLoading,
40
+ chatId,
41
+ setChatId,
42
+ messages,
43
+ }: ChatTopbarProps) {
44
+ const hasMounted = useHasMounted();
45
+
46
+ const currentModel = chatOptions && chatOptions.selectedModel;
47
+ const [error, setError] = React.useState<string | undefined>(undefined);
48
+
49
+ const fetchData = async () => {
50
+ if (!hasMounted) {
51
+ return null;
52
+ }
53
+ try {
54
+ const res = await fetch(basePath + "/api/models");
55
+
56
+ if (!res.ok) {
57
+ const errorResponse = await res.json();
58
+ const errorMessage = `Connection to vLLM server failed: ${errorResponse.error} [${res.status} ${res.statusText}]`;
59
+ throw new Error(errorMessage);
60
+ }
61
+
62
+ const data = await res.json();
63
+ // Extract the "name" field from each model object and store them in the state
64
+ const modelNames = data.data.map((model: any) => model.id);
65
+ // save the first and only model in the list as selectedModel in localstorage
66
+ setChatOptions({ ...chatOptions, selectedModel: modelNames[0] });
67
+ } catch (error) {
68
+ setChatOptions({ ...chatOptions, selectedModel: undefined });
69
+ toast.error(error as string);
70
+ }
71
+ };
72
+
73
+ useEffect(() => {
74
+ fetchData();
75
+ }, [hasMounted]);
76
+
77
+ if (!hasMounted) {
78
+ return (
79
+ <div className="md:w-full flex px-4 py-6 items-center gap-1 md:justify-center">
80
+ <DotFilledIcon className="w-4 h-4 text-blue-500" />
81
+ <span className="text-xs">Booting up..</span>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ const chatTokens = messages.length > 0 ? encodeChat(messages) : 0;
87
+
88
+ return (
89
+ <div className="md:w-full flex px-4 py-4 items-center justify-between md:justify-center">
90
+ <Sheet>
91
+ <SheetTrigger>
92
+ <div className="flex items-center gap-2">
93
+ <HamburgerMenuIcon className="md:hidden w-5 h-5" />
94
+ </div>
95
+ </SheetTrigger>
96
+ <SheetContent side="left">
97
+ <div>
98
+ <Sidebar
99
+ chatId={chatId || ""}
100
+ setChatId={setChatId}
101
+ isCollapsed={false}
102
+ isMobile={false}
103
+ chatOptions={chatOptions}
104
+ setChatOptions={setChatOptions}
105
+ />
106
+ </div>
107
+ </SheetContent>
108
+ </Sheet>
109
+
110
+ <div className="flex justify-center md:justify-between gap-4 w-full">
111
+ <div className="gap-1 flex items-center">
112
+ {currentModel !== undefined && (
113
+ <>
114
+ {isLoading ? (
115
+ <DotFilledIcon className="w-4 h-4 text-blue-500" />
116
+ ) : (
117
+ <TooltipProvider>
118
+ <Tooltip>
119
+ <TooltipTrigger>
120
+ <span className="cursor-help">
121
+ <CheckCircledIcon className="w-4 h-4 text-green-500" />
122
+ </span>
123
+ </TooltipTrigger>
124
+ <TooltipContent
125
+ sideOffset={4}
126
+ className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 p-2 rounded-sm text-xs"
127
+ >
128
+ <p className="font-bold">Current Model</p>
129
+ <p className="text-gray-500">{currentModel}</p>
130
+ </TooltipContent>
131
+ </Tooltip>
132
+ </TooltipProvider>
133
+ )}
134
+ <span className="text-xs">
135
+ {isLoading ? "Generating.." : "Ready"}
136
+ </span>
137
+ </>
138
+ )}
139
+ {currentModel === undefined && (
140
+ <>
141
+ <CrossCircledIcon className="w-4 h-4 text-red-500" />
142
+ <span className="text-xs">Connection to vLLM server failed</span>
143
+ </>
144
+ )}
145
+ </div>
146
+ <div className="flex items-end gap-2">
147
+ {chatTokens > tokenLimit && (
148
+ <TooltipProvider>
149
+ <Tooltip>
150
+ <TooltipTrigger>
151
+ <span>
152
+ <InfoCircledIcon className="w-4 h-4 text-blue-500" />
153
+ </span>
154
+ </TooltipTrigger>
155
+ <TooltipContent
156
+ sideOffset={4}
157
+ className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-sm text-xs"
158
+ >
159
+ <p className="text-gray-500">
160
+ Token limit exceeded. Truncating middle messages.
161
+ </p>
162
+ </TooltipContent>
163
+ </Tooltip>
164
+ </TooltipProvider>
165
+ )}
166
+ {messages.length > 0 && (
167
+ <span className="text-xs text-gray-500">
168
+ {chatTokens} / {tokenLimit} token{chatTokens > 1 ? "s" : ""}
169
+ </span>
170
+ )}
171
+ </div>
172
+ </div>
173
+ </div>
174
+ );
175
+ }
src/components/chat/chat.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ import { ChatRequestOptions } from "ai";
4
+ import { Message } from "ai/react";
5
+
6
+ import ChatBottombar from "./chat-bottombar";
7
+ import ChatList from "./chat-list";
8
+ import { ChatOptions } from "./chat-options";
9
+ import ChatTopbar from "./chat-topbar";
10
+
11
+ export interface ChatProps {
12
+ chatId?: string;
13
+ setChatId: React.Dispatch<React.SetStateAction<string>>;
14
+ messages: Message[];
15
+ input: string;
16
+ handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
17
+ handleSubmit: (
18
+ e: React.FormEvent<HTMLFormElement>,
19
+ chatRequestOptions?: ChatRequestOptions
20
+ ) => void;
21
+ isLoading: boolean;
22
+ error: undefined | Error;
23
+ stop: () => void;
24
+ }
25
+
26
+ export interface ChatTopbarProps {
27
+ chatOptions: ChatOptions;
28
+ setChatOptions: React.Dispatch<React.SetStateAction<ChatOptions>>;
29
+ }
30
+
31
+ export default function Chat({
32
+ messages,
33
+ input,
34
+ handleInputChange,
35
+ handleSubmit,
36
+ isLoading,
37
+ error,
38
+ stop,
39
+ chatOptions,
40
+ setChatOptions,
41
+ chatId,
42
+ setChatId,
43
+ }: ChatProps & ChatTopbarProps) {
44
+ return (
45
+ <div className="flex flex-col justify-between w-full h-full ">
46
+ <ChatTopbar
47
+ chatOptions={chatOptions}
48
+ setChatOptions={setChatOptions}
49
+ isLoading={isLoading}
50
+ chatId={chatId}
51
+ setChatId={setChatId}
52
+ messages={messages}
53
+ />
54
+
55
+ <ChatList
56
+ messages={messages}
57
+ isLoading={isLoading}
58
+ />
59
+
60
+ <ChatBottombar
61
+ selectedModel={chatOptions.selectedModel}
62
+ input={input}
63
+ handleInputChange={handleInputChange}
64
+ handleSubmit={handleSubmit}
65
+ isLoading={isLoading}
66
+ stop={stop}
67
+ />
68
+ </div>
69
+ );
70
+ }
src/components/code-display-block.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import React from "react";
3
+
4
+ import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
5
+ import { useTheme } from "next-themes";
6
+ import { CodeBlock, dracula, github } from "react-code-blocks";
7
+ import { toast } from "sonner";
8
+
9
+ import { Button } from "./ui/button";
10
+
11
+ interface ButtonCodeblockProps {
12
+ code: string;
13
+ lang: string;
14
+ }
15
+
16
+ export default function CodeDisplayBlock({ code, lang }: ButtonCodeblockProps) {
17
+ const [isCopied, setisCopied] = React.useState(false);
18
+ const { theme } = useTheme();
19
+
20
+ const copyToClipboard = () => {
21
+ navigator.clipboard.writeText(code);
22
+ setisCopied(true);
23
+ toast.success("Code copied to clipboard!");
24
+ setTimeout(() => {
25
+ setisCopied(false);
26
+ }, 1500);
27
+ };
28
+
29
+ return (
30
+ <div className="relative my-4 overflow-scroll overflow-x-scroll flex flex-col text-start ">
31
+ <CodeBlock
32
+ customStyle={
33
+ theme === "dark"
34
+ ? { background: "#303033" }
35
+ : { background: "#fcfcfc" }
36
+ }
37
+ text={code}
38
+ language="tsx"
39
+ showLineNumbers={false}
40
+ theme={theme === "dark" ? dracula : github}
41
+ />
42
+ <Button
43
+ onClick={copyToClipboard}
44
+ variant="ghost"
45
+ size="iconSm"
46
+ className="h-5 w-5 absolute top-2 right-2"
47
+ >
48
+ {isCopied ? (
49
+ <CheckIcon className="w-4 h-4" />
50
+ ) : (
51
+ <CopyIcon className="w-4 h-4" />
52
+ )}
53
+ </Button>
54
+ </div>
55
+ );
56
+ }
src/components/settings-clear-chats.tsx ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import {
6
+ Dialog,
7
+ DialogClose,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogTrigger,
11
+ } from "@radix-ui/react-dialog";
12
+ import { TrashIcon } from "@radix-ui/react-icons";
13
+ import { useRouter } from "next/navigation";
14
+
15
+ import { useHasMounted } from "@/lib/utils";
16
+ import { DialogHeader } from "./ui/dialog";
17
+
18
+ export default function ClearChatsButton() {
19
+ const hasMounted = useHasMounted();
20
+ const router = useRouter();
21
+
22
+ if (!hasMounted) {
23
+ return null;
24
+ }
25
+
26
+ const chats = Object.keys(localStorage).filter((key) =>
27
+ key.startsWith("chat_")
28
+ );
29
+
30
+ const disabled = chats.length === 0;
31
+
32
+ const clearChats = () => {
33
+ chats.forEach((key) => {
34
+ localStorage.removeItem(key);
35
+ });
36
+ window.dispatchEvent(new Event("storage"));
37
+ router.push("/");
38
+ };
39
+
40
+ return (
41
+ <Dialog>
42
+ <DialogTrigger
43
+ className="inline-flex items-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 rounded-sm px-3 text-xs justify-start gap-2 w-full"
44
+ disabled={disabled}
45
+ >
46
+ <TrashIcon className="w-4 h-4" />
47
+ <span>Clear chats</span>
48
+ </DialogTrigger>
49
+ <DialogContent>
50
+ <DialogHeader className="space-y-2">
51
+ <DialogDescription className="text-xs">
52
+ Are you sure you want to delete all chats? This action cannot be
53
+ undone.
54
+ </DialogDescription>
55
+ <div className="flex justify-end gap-2">
56
+ <DialogClose className="border border-input bg-background hover:bg-accent hover:text-accent-foreground px-3 py-2 rounded-sm text-xs">
57
+ Cancel
58
+ </DialogClose>
59
+ <DialogClose
60
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-3 py-2 rounded-sm text-xs"
61
+ onClick={() => clearChats()}
62
+ >
63
+ Delete
64
+ </DialogClose>
65
+ </div>
66
+ </DialogHeader>
67
+ </DialogContent>
68
+ </Dialog>
69
+ );
70
+ }
src/components/settings-theme-toggle.tsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
6
+ import { useTheme } from "next-themes";
7
+
8
+ import { useHasMounted } from "@/lib/utils";
9
+ import { Button } from "./ui/button";
10
+
11
+ export default function SettingsThemeToggle() {
12
+ const hasMounted = useHasMounted();
13
+ const { setTheme, theme } = useTheme();
14
+
15
+ if (!hasMounted) {
16
+ return null;
17
+ }
18
+
19
+ const nextTheme = theme === "light" ? "dark" : "light";
20
+
21
+ return (
22
+ <Button
23
+ className="justify-start gap-2 w-full"
24
+ size="sm"
25
+ variant="ghost"
26
+ onClick={() => setTheme(nextTheme)}
27
+ >
28
+ {nextTheme === "light" ? (
29
+ <SunIcon className="w-4 h-4" />
30
+ ) : (
31
+ <MoonIcon className="w-4 h-4" />
32
+ )}
33
+ <p>{nextTheme === "light" ? "Light mode" : "Dark mode"}</p>
34
+ </Button>
35
+ );
36
+ }
src/components/settings.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import ClearChatsButton from "./settings-clear-chats";
4
+ import SettingsThemeToggle from "./settings-theme-toggle";
5
+ import SystemPrompt, { SystemPromptProps } from "./system-prompt";
6
+ import { Input } from "./ui/input";
7
+
8
+ const TemperatureSlider = ({
9
+ chatOptions,
10
+ setChatOptions,
11
+ }: SystemPromptProps) => {
12
+ const handleTemperatureChange = (e: React.ChangeEvent<HTMLInputElement>) => {
13
+ setChatOptions({ ...chatOptions, temperature: parseFloat(e.target.value) });
14
+ };
15
+
16
+ return (
17
+ <div>
18
+ <div className="mx-2 flex align-middle gap-4 items-center justify-between
19
+ ">
20
+ <label
21
+ htmlFor="small-input"
22
+ className="text-xs font-medium text-gray-900 dark:text-white align-middle"
23
+ >
24
+ Temperature
25
+ </label>
26
+ <Input
27
+ type="text"
28
+ id="small-input"
29
+ className="w-1/4 text-gray-900 hover:border hover:border-gray-300 rounded-sm hover:bg-gray-200 text-xs focus:ring-blue-500 focus:border-blue-500 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 h-6
30
+ text-right
31
+ "
32
+ value={chatOptions.temperature}
33
+ onChange={handleTemperatureChange}
34
+ min={0}
35
+ max={2}
36
+ step={0.1}
37
+ />
38
+ </div>
39
+
40
+ <div className="p-2">
41
+ <input
42
+ id="labels-range-input"
43
+ type="range"
44
+ value={chatOptions.temperature}
45
+ onChange={handleTemperatureChange}
46
+ min="0.0"
47
+ max="2.0"
48
+ step="0.1"
49
+ className="w-full h-1 bg-gray-200 rounded-sm appearance-none cursor-pointer range-sm dark:bg-gray-700"
50
+ />
51
+ </div>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export default function Settings({
57
+ chatOptions,
58
+ setChatOptions,
59
+ }: SystemPromptProps) {
60
+ return (
61
+ <>
62
+ <SystemPrompt chatOptions={chatOptions} setChatOptions={setChatOptions} />
63
+ <TemperatureSlider
64
+ chatOptions={chatOptions}
65
+ setChatOptions={setChatOptions}
66
+ />
67
+ <SettingsThemeToggle />
68
+ <ClearChatsButton />
69
+ </>
70
+ );
71
+ }
src/components/sidebar-skeleton.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Skeleton } from "@/components/ui/skeleton";
2
+
3
+ export default function SidebarSkeleton() {
4
+ return (
5
+ <div className="flex flex-col w-full gap-2 ">
6
+ <div className="flex h-14 w-full bg-primary/5 justify-between rounded-md items-center p-2">
7
+ <Skeleton className="h-6 w-2/3 rounded-sm" />
8
+ <Skeleton className="h-6 w-6 rounded-md" />
9
+ </div>
10
+
11
+ <div className="flex h-14 w-full bg-primary/5 opacity-80 justify-between rounded-md items-center p-2">
12
+ <Skeleton className="h-6 w-2/3 rounded-sm" />
13
+ <Skeleton className="h-6 w-6 rounded-md" />
14
+ </div>
15
+
16
+ <div className="flex h-14 w-full bg-primary/5 opacity-60 justify-between rounded-md items-center p-2">
17
+ <Skeleton className="h-6 w-2/3 rounded-sm" />
18
+ <Skeleton className="h-6 w-6 rounded-md" />
19
+ </div>
20
+
21
+ <div className="flex h-14 w-full bg-primary/5 opacity-40 justify-between rounded-md items-center p-2">
22
+ <Skeleton className="h-6 w-2/3 rounded-sm" />
23
+ <Skeleton className="h-6 w-6 rounded-md" />
24
+ </div>
25
+
26
+ <div className="flex h-14 w-full bg-primary/5 opacity-20 justify-between rounded-md items-center p-2">
27
+ <Skeleton className="h-6 w-2/3 rounded-sm" />
28
+ <Skeleton className="h-6 w-6 rounded-md" />
29
+ </div>
30
+
31
+ </div>
32
+ );
33
+ }
src/components/sidebar-tabs.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ import { DialogClose } from "@radix-ui/react-dialog";
6
+ import { ChatBubbleIcon, GearIcon, TrashIcon } from "@radix-ui/react-icons";
7
+ import * as Tabs from "@radix-ui/react-tabs";
8
+ import { Message } from "ai/react";
9
+ import Link from "next/link";
10
+
11
+ import { buttonVariants } from "@/components/ui/button";
12
+ import { cn } from "@/lib/utils";
13
+ import { ChatOptions } from "./chat/chat-options";
14
+ import Settings from "./settings";
15
+ import SidebarSkeleton from "./sidebar-skeleton";
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ DialogTrigger,
23
+ } from "./ui/dialog";
24
+
25
+ interface Chats {
26
+ [key: string]: { chatId: string; messages: Message[] }[];
27
+ }
28
+ interface SidebarTabsProps {
29
+ isLoading: boolean;
30
+ localChats: Chats;
31
+ selectedChatId: string;
32
+ chatOptions: ChatOptions;
33
+ setChatOptions: React.Dispatch<React.SetStateAction<ChatOptions>>;
34
+ handleDeleteChat: (chatId: string) => void;
35
+ }
36
+
37
+ const SidebarTabs = ({
38
+ localChats,
39
+ selectedChatId,
40
+ isLoading,
41
+ chatOptions,
42
+ setChatOptions,
43
+ handleDeleteChat,
44
+ }: SidebarTabsProps) => (
45
+ <Tabs.Root
46
+ className="overflow-hidden h-full bg-accent/20 dark:bg-card/35"
47
+ defaultValue="chats"
48
+ >
49
+ <div className=" text-sm o h-full">
50
+ <Tabs.Content className="h-screen overflow-y-auto" value="chats">
51
+ <div className="h-full mb-28">
52
+ {Object.keys(localChats).length > 0 && (
53
+ <>
54
+ {Object.keys(localChats).map((group, index) => (
55
+ <div key={index} className="flex flex-col py-2 pl-3 pr-1 gap-2 mb-8">
56
+ <p className="px-2 text-xs font-medium text-muted-foreground">
57
+ {group}
58
+ </p>
59
+ <ol>
60
+ {localChats[group].map(
61
+ ({ chatId, messages }, chatIndex) => {
62
+ const isSelected =
63
+ chatId.substring(5) === selectedChatId;
64
+ return (
65
+ <li
66
+ className="flex w-full items-center relative"
67
+ key={chatIndex}
68
+ >
69
+ <div className="flex-col w-full truncate">
70
+ <Link
71
+ href={`/chats/${chatId.substring(5)}`}
72
+ className={cn(
73
+ {
74
+ [buttonVariants({
75
+ variant: "secondaryLink",
76
+ })]: isSelected,
77
+ [buttonVariants({ variant: "ghost" })]:
78
+ !isSelected,
79
+ },
80
+ "flex gap-2 p-2 justify-start"
81
+ )}
82
+ >
83
+ <span className="text-sm font-normal max-w-[184px] truncate">
84
+ {messages.length > 0
85
+ ? messages[0].content
86
+ : ""}
87
+ </span>
88
+ </Link>
89
+ </div>
90
+ <div className="absolute right-0 rounded-xs">
91
+ <Dialog>
92
+ <DialogTrigger
93
+ className={
94
+ "items-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-8 rounded-sm px-3 text-xs justify-start gap-2 w-full hover:text-red-500 hover:bg-card dark:hover:bg-accent" +
95
+ (isSelected
96
+ ? " bg-accent dark:bg-card"
97
+ : " ")
98
+ }
99
+ >
100
+ <TrashIcon className="w-4 h-4" />
101
+ </DialogTrigger>
102
+ <DialogContent>
103
+ <DialogHeader className="space-y-4">
104
+ <DialogTitle>Delete chat?</DialogTitle>
105
+ <DialogDescription>
106
+ Are you sure you want to delete this chat?
107
+ This action cannot be undone.
108
+ </DialogDescription>
109
+ <div className="flex justify-end gap-2">
110
+ <DialogClose className="border border-input bg-background hover:bg-accent hover:text-accent-foreground px-4 py-2 rounded-sm">
111
+ Cancel
112
+ </DialogClose>
113
+
114
+ <DialogClose
115
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90 px-4 py-2 rounded-sm"
116
+ onClick={() => handleDeleteChat(chatId)}
117
+ >
118
+ Delete
119
+ </DialogClose>
120
+ </div>
121
+ </DialogHeader>
122
+ </DialogContent>
123
+ </Dialog>
124
+ </div>
125
+ </li>
126
+ );
127
+ }
128
+ )}
129
+ </ol>
130
+ </div>
131
+ ))}
132
+ </>
133
+ )}
134
+ {isLoading && <SidebarSkeleton />}
135
+ </div>
136
+ </Tabs.Content>
137
+ <Tabs.Content className="h-screen overflow-y-auto" value="settings">
138
+ <div className="h-full mb-16 pl-2">
139
+ <Settings chatOptions={chatOptions} setChatOptions={setChatOptions} />
140
+ </div>
141
+ </Tabs.Content>
142
+ </div>
143
+ <div className="sticky left-0 right-0 bottom-0 z-20 m-0 overflow-hidden">
144
+ <Tabs.List
145
+ className="flex flex-wrap -mb-px py-2 text-sm font-medium text-center justify-center gap-2 bg-accent dark:bg-card"
146
+ aria-label="Navigation"
147
+ >
148
+ <Tabs.Trigger
149
+ className="inline-flex items-center justify-center p-0.5 rounded-sm data-[state=active]:bg-gray-200 dark:data-[state=active]:bg-gray-700 h-10 w-10"
150
+ value="chats"
151
+ >
152
+ <ChatBubbleIcon className="w-5 h-5" />
153
+ {/* <span className="text-xs">Chats</span> */}
154
+ </Tabs.Trigger>
155
+ <Tabs.Trigger
156
+ className="inline-flex items-center justify-center p-0.5 rounded-sm data-[state=active]:bg-gray-200 dark:data-[state=active]:bg-gray-700 h-10 w-10"
157
+ value="settings"
158
+ >
159
+ <GearIcon className="w-5 h-5" />
160
+ {/* <span className="text-xs">Settings</span> */}
161
+ </Tabs.Trigger>
162
+ </Tabs.List>
163
+ </div>
164
+ </Tabs.Root>
165
+ );
166
+
167
+ export default SidebarTabs;
src/components/sidebar.tsx ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useState } from "react";
3
+
4
+ import { Pencil2Icon } from "@radix-ui/react-icons";
5
+ import { Message } from "ai/react";
6
+ import Image from "next/image";
7
+
8
+ import OllamaLogo from "../../public/ollama.png";
9
+ import { ChatOptions } from "./chat/chat-options";
10
+ import SidebarTabs from "./sidebar-tabs";
11
+ import Link from "next/link";
12
+
13
+ interface SidebarProps {
14
+ isCollapsed: boolean;
15
+ onClick?: () => void;
16
+ isMobile: boolean;
17
+ chatId: string;
18
+ setChatId: React.Dispatch<React.SetStateAction<string>>;
19
+ chatOptions: ChatOptions;
20
+ setChatOptions: React.Dispatch<React.SetStateAction<ChatOptions>>;
21
+ }
22
+
23
+ interface Chats {
24
+ [key: string]: { chatId: string; messages: Message[] }[];
25
+ }
26
+
27
+ export function Sidebar({
28
+ isCollapsed,
29
+ isMobile,
30
+ chatId,
31
+ setChatId,
32
+ chatOptions,
33
+ setChatOptions,
34
+ }: SidebarProps) {
35
+ const [localChats, setLocalChats] = useState<Chats>({});
36
+ const [isLoading, setIsLoading] = useState(true);
37
+
38
+ useEffect(() => {
39
+ setLocalChats(getLocalstorageChats());
40
+ const handleStorageChange = () => {
41
+ setLocalChats(getLocalstorageChats());
42
+ };
43
+ window.addEventListener("storage", handleStorageChange);
44
+ return () => {
45
+ window.removeEventListener("storage", handleStorageChange);
46
+ };
47
+ }, [chatId]);
48
+
49
+ const getLocalstorageChats = (): Chats => {
50
+ const chats = Object.keys(localStorage).filter((key) =>
51
+ key.startsWith("chat_")
52
+ );
53
+
54
+ if (chats.length === 0) {
55
+ setIsLoading(false);
56
+ }
57
+
58
+ // Map through the chats and return an object with chatId and messages
59
+ const chatObjects = chats.map((chat) => {
60
+ const item = localStorage.getItem(chat);
61
+ return item
62
+ ? { chatId: chat, messages: JSON.parse(item) }
63
+ : { chatId: "", messages: [] };
64
+ });
65
+
66
+ // Sort chats by the createdAt date of the first message of each chat
67
+ chatObjects.sort((a, b) => {
68
+ const aDate = new Date(a.messages[0].createdAt);
69
+ const bDate = new Date(b.messages[0].createdAt);
70
+ return bDate.getTime() - aDate.getTime();
71
+ });
72
+
73
+ const groupChatsByDate = (
74
+ chats: { chatId: string; messages: Message[] }[]
75
+ ) => {
76
+ const today = new Date();
77
+ const yesterday = new Date(today);
78
+ yesterday.setDate(yesterday.getDate() - 1);
79
+
80
+ const groupedChats: Chats = {};
81
+
82
+ chats.forEach((chat) => {
83
+ const createdAt = new Date(chat.messages[0].createdAt ?? "");
84
+ const diffInDays = Math.floor(
85
+ (today.getTime() - createdAt.getTime()) / (1000 * 3600 * 24)
86
+ );
87
+
88
+ let group: string;
89
+ if (diffInDays === 0) {
90
+ group = "Today";
91
+ } else if (diffInDays === 1) {
92
+ group = "Yesterday";
93
+ } else if (diffInDays <= 7) {
94
+ group = "Previous 7 Days";
95
+ } else if (diffInDays <= 30) {
96
+ group = "Previous 30 Days";
97
+ } else {
98
+ group = "Older";
99
+ }
100
+
101
+ if (!groupedChats[group]) {
102
+ groupedChats[group] = [];
103
+ }
104
+ groupedChats[group].push(chat);
105
+ });
106
+
107
+ return groupedChats;
108
+ };
109
+
110
+ setIsLoading(false);
111
+ const groupedChats = groupChatsByDate(chatObjects);
112
+
113
+ return groupedChats;
114
+ };
115
+
116
+ const handleDeleteChat = (chatId: string) => {
117
+ localStorage.removeItem(chatId);
118
+ setLocalChats(getLocalstorageChats());
119
+ };
120
+
121
+ return (
122
+ <div
123
+ data-collapsed={isCollapsed}
124
+ className="relative justify-between group bg-accent/20 dark:bg-card/35 flex flex-col h-full gap-4 data-[collapsed=true]:p-0 data-[collapsed=true]:hidden"
125
+ >
126
+ <div className="sticky left-0 right-0 top-0 z-20 p-1 rounded-sm m-2">
127
+ <Link
128
+ className="flex w-full h-10 text-sm font-medium items-center
129
+ border border-input bg-background hover:bg-accent hover:text-accent-foreground
130
+ px-2 py-2 rounded-sm"
131
+ href="/"
132
+ onClick={() => {
133
+ setChatId("");
134
+ }}
135
+ >
136
+ <div className="flex gap-3 p-2 items-center justify-between w-full">
137
+ <div className="flex align-start gap-2">
138
+ {!isCollapsed && !isMobile && (
139
+ <Image
140
+ src={OllamaLogo}
141
+ alt="AI"
142
+ width={14}
143
+ height={14}
144
+ className="dark:invert 2xl:block"
145
+ />
146
+ )}
147
+ <span>New chat</span>
148
+ </div>
149
+ <Pencil2Icon className="w-4 h-4" />
150
+ </div>
151
+ </Link>
152
+ </div>
153
+ <SidebarTabs
154
+ isLoading={isLoading}
155
+ localChats={localChats}
156
+ selectedChatId={chatId}
157
+ chatOptions={chatOptions}
158
+ setChatOptions={setChatOptions}
159
+ handleDeleteChat={handleDeleteChat}
160
+ />
161
+ </div>
162
+ );
163
+ }
src/components/system-prompt.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Dispatch, SetStateAction, useEffect, useState } from "react";
4
+
5
+ import { toast } from "sonner";
6
+ import { useDebounce } from "use-debounce";
7
+
8
+ import { useHasMounted } from "@/lib/utils";
9
+ import { ChatOptions } from "./chat/chat-options";
10
+ import { Textarea } from "./ui/textarea";
11
+
12
+ export interface SystemPromptProps {
13
+ chatOptions: ChatOptions;
14
+ setChatOptions: Dispatch<SetStateAction<ChatOptions>>;
15
+ }
16
+ export default function SystemPrompt({
17
+ chatOptions,
18
+ setChatOptions,
19
+ }: SystemPromptProps) {
20
+ const hasMounted = useHasMounted();
21
+
22
+ const systemPrompt = chatOptions ? chatOptions.systemPrompt : "";
23
+ const [text, setText] = useState<string>(systemPrompt || "");
24
+ const [debouncedText] = useDebounce(text, 500);
25
+
26
+ useEffect(() => {
27
+ if (!hasMounted) {
28
+ return;
29
+ }
30
+ if (debouncedText !== systemPrompt) {
31
+ setChatOptions({ ...chatOptions, systemPrompt: debouncedText });
32
+ toast.success("System prompt updated", { duration: 1000 });
33
+ }
34
+ }, [hasMounted, debouncedText]);
35
+
36
+ return (
37
+ <div>
38
+ <div className="justify-start gap-2 w-full rounded-sm px-2 text-xs">
39
+ <p>System prompt</p>
40
+ </div>
41
+
42
+ <div className="m-2">
43
+ <Textarea
44
+ className="resize-none bg-white/20 dark:bg-card/35"
45
+ autoComplete="off"
46
+ rows={7}
47
+ value={text}
48
+ onChange={(e) => setText(e.currentTarget.value)}
49
+ name="systemPrompt"
50
+ placeholder="You are a helpful assistant."
51
+ />
52
+ </div>
53
+ </div>
54
+ );
55
+ }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center whitespace-nowrap rounded-sm text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground hover:bg-primary/90 border border-black bg-black p-0.5 text-white transition-colors enabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:hover:bg-white",
14
+ destructive:
15
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
16
+ outline:
17
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
18
+ secondary:
19
+ "dark:bg-card/60 bg-accent/20 text-secondary-foreground hover:bg-secondary/60 hover:dark:bg-card/40",
20
+ ghost: "hover:bg-accent hover:text-accent-foreground",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ secondaryLink: "bg-accent/90 dark:bg-secondary/80 text-secondary-foreground dark:hover:bg-secondary hover:bg-accent",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2",
26
+ sm: "h-8 rounded-sm px-3 text-xs",
27
+ lg: "h-10 rounded-sm px-8",
28
+ icon: "h-10 w-10",
29
+ iconSm: "h-8 w-8"
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ export interface ButtonProps
40
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
41
+ VariantProps<typeof buttonVariants> {
42
+ asChild?: boolean
43
+ }
44
+
45
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
46
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
47
+ const Comp = asChild ? Slot : "button"
48
+ return (
49
+ <Comp
50
+ className={cn(buttonVariants({ variant, size, className }))}
51
+ ref={ref}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+ )
57
+ Button.displayName = "Button"
58
+
59
+ export { Button, buttonVariants }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ const Card = React.forwardRef<
6
+ HTMLDivElement,
7
+ React.HTMLAttributes<HTMLDivElement>
8
+ >(({ className, ...props }, ref) => (
9
+ <div
10
+ ref={ref}
11
+ className={cn(
12
+ "rounded-md border bg-card text-card-foreground shadow",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ ))
18
+ Card.displayName = "Card"
19
+
20
+ const CardHeader = React.forwardRef<
21
+ HTMLDivElement,
22
+ React.HTMLAttributes<HTMLDivElement>
23
+ >(({ className, ...props }, ref) => (
24
+ <div
25
+ ref={ref}
26
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
27
+ {...props}
28
+ />
29
+ ))
30
+ CardHeader.displayName = "CardHeader"
31
+
32
+ const CardTitle = React.forwardRef<
33
+ HTMLParagraphElement,
34
+ React.HTMLAttributes<HTMLHeadingElement>
35
+ >(({ className, ...props }, ref) => (
36
+ <h3
37
+ ref={ref}
38
+ className={cn("font-semibold leading-none tracking-tight", className)}
39
+ {...props}
40
+ />
41
+ ))
42
+ CardTitle.displayName = "CardTitle"
43
+
44
+ const CardDescription = React.forwardRef<
45
+ HTMLParagraphElement,
46
+ React.HTMLAttributes<HTMLParagraphElement>
47
+ >(({ className, ...props }, ref) => (
48
+ <p
49
+ ref={ref}
50
+ className={cn("text-sm text-muted-foreground", className)}
51
+ {...props}
52
+ />
53
+ ))
54
+ CardDescription.displayName = "CardDescription"
55
+
56
+ const CardContent = React.forwardRef<
57
+ HTMLDivElement,
58
+ React.HTMLAttributes<HTMLDivElement>
59
+ >(({ className, ...props }, ref) => (
60
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
61
+ ))
62
+ CardContent.displayName = "CardContent"
63
+
64
+ const CardFooter = React.forwardRef<
65
+ HTMLDivElement,
66
+ React.HTMLAttributes<HTMLDivElement>
67
+ >(({ className, ...props }, ref) => (
68
+ <div
69
+ ref={ref}
70
+ className={cn("flex items-center p-6 pt-0", className)}
71
+ {...props}
72
+ />
73
+ ))
74
+ CardFooter.displayName = "CardFooter"
75
+
76
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
src/components/ui/dialog.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import * as DialogPrimitive from "@radix-ui/react-dialog"
6
+ import { Cross2Icon } from "@radix-ui/react-icons"
7
+
8
+ import { cn } from "@/lib/utils"
9
+
10
+ const Dialog = DialogPrimitive.Root
11
+
12
+ const DialogTrigger = DialogPrimitive.Trigger
13
+
14
+ const DialogPortal = DialogPrimitive.Portal
15
+
16
+ const DialogClose = DialogPrimitive.Close
17
+
18
+ const DialogOverlay = React.forwardRef<
19
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
20
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
21
+ >(({ className, ...props }, ref) => (
22
+ <DialogPrimitive.Overlay
23
+ ref={ref}
24
+ className={cn(
25
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
26
+ className
27
+ )}
28
+ {...props}
29
+ />
30
+ ))
31
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
32
+
33
+ const DialogContent = React.forwardRef<
34
+ React.ElementRef<typeof DialogPrimitive.Content>,
35
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
36
+ >(({ className, children, ...props }, ref) => (
37
+ <DialogPortal>
38
+ <DialogOverlay />
39
+ <DialogPrimitive.Content
40
+ ref={ref}
41
+ className={cn(
42
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ {children}
48
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
49
+ <Cross2Icon className="h-4 w-4" />
50
+ <span className="sr-only">Close</span>
51
+ </DialogPrimitive.Close>
52
+ </DialogPrimitive.Content>
53
+ </DialogPortal>
54
+ ))
55
+ DialogContent.displayName = DialogPrimitive.Content.displayName
56
+
57
+ const DialogHeader = ({
58
+ className,
59
+ ...props
60
+ }: React.HTMLAttributes<HTMLDivElement>) => (
61
+ <div
62
+ className={cn(
63
+ "flex flex-col space-y-1.5 text-start sm:text-left",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ )
69
+ DialogHeader.displayName = "DialogHeader"
70
+
71
+ const DialogFooter = ({
72
+ className,
73
+ ...props
74
+ }: React.HTMLAttributes<HTMLDivElement>) => (
75
+ <div
76
+ className={cn(
77
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
78
+ className
79
+ )}
80
+ {...props}
81
+ />
82
+ )
83
+ DialogFooter.displayName = "DialogFooter"
84
+
85
+ const DialogTitle = React.forwardRef<
86
+ React.ElementRef<typeof DialogPrimitive.Title>,
87
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
88
+ >(({ className, ...props }, ref) => (
89
+ <DialogPrimitive.Title
90
+ ref={ref}
91
+ className={cn(
92
+ "text-lg font-semibold leading-none tracking-tight",
93
+ className
94
+ )}
95
+ {...props}
96
+ />
97
+ ))
98
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
99
+
100
+ const DialogDescription = React.forwardRef<
101
+ React.ElementRef<typeof DialogPrimitive.Description>,
102
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
103
+ >(({ className, ...props }, ref) => (
104
+ <DialogPrimitive.Description
105
+ ref={ref}
106
+ className={cn("text-sm text-muted-foreground", className)}
107
+ {...props}
108
+ />
109
+ ))
110
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
111
+
112
+ export {
113
+ Dialog, DialogClose,
114
+ DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger
115
+ }
src/components/ui/input.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {}
7
+
8
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
+ ({ className, type, ...props }, ref) => {
10
+ return (
11
+ <input
12
+ type={type}
13
+ className={cn(
14
+ "flex h-9 w-full rounded-sm border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
15
+ className
16
+ )}
17
+ ref={ref}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+ )
23
+ Input.displayName = "Input"
24
+
25
+ export { Input }
src/components/ui/sheet.tsx ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import * as SheetPrimitive from "@radix-ui/react-dialog";
6
+ import { CrossCircledIcon } from "@radix-ui/react-icons";
7
+ import { cva, type VariantProps } from "class-variance-authority";
8
+
9
+ import { cn } from "@/lib/utils";
10
+
11
+ const Sheet = SheetPrimitive.Root;
12
+ const SheetTrigger = SheetPrimitive.Trigger;
13
+ const SheetClose = SheetPrimitive.Close;
14
+ const SheetPortal = SheetPrimitive.Portal;
15
+
16
+ const SheetOverlay = React.forwardRef<
17
+ React.ElementRef<typeof SheetPrimitive.Overlay>,
18
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
19
+ >(({ className, ...props }, ref) => (
20
+ <SheetPrimitive.Overlay
21
+ className={cn(
22
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
+ className
24
+ )}
25
+ {...props}
26
+ ref={ref}
27
+ />
28
+ ));
29
+ SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
30
+
31
+ const sheetVariants = cva(
32
+ "fixed z-50 gap-4 bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33
+ {
34
+ variants: {
35
+ side: {
36
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37
+ bottom:
38
+ "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40
+ right:
41
+ "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42
+ },
43
+ },
44
+ defaultVariants: {
45
+ side: "right",
46
+ },
47
+ }
48
+ );
49
+
50
+ interface SheetContentProps
51
+ extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
52
+ VariantProps<typeof sheetVariants> {}
53
+
54
+ const SheetContent = React.forwardRef<
55
+ React.ElementRef<typeof SheetPrimitive.Content>,
56
+ SheetContentProps
57
+ >(({ side = "right", className, children, ...props }, ref) => (
58
+ <SheetPortal>
59
+ <SheetOverlay />
60
+ <SheetPrimitive.Content
61
+ ref={ref}
62
+ className={cn(sheetVariants({ side }), className)}
63
+ {...props}
64
+ >
65
+ {children}
66
+ </SheetPrimitive.Content>
67
+ <SheetPrimitive.Close className="absolute w-1/4 pl-4 pt-4 z-200 right-0 top-0 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
68
+ <CrossCircledIcon className="h-8 w-8" />
69
+ <span className="sr-only">Close</span>
70
+ </SheetPrimitive.Close>
71
+ </SheetPortal>
72
+ ));
73
+ SheetContent.displayName = SheetPrimitive.Content.displayName;
74
+
75
+ const SheetHeader = ({
76
+ className,
77
+ ...props
78
+ }: React.HTMLAttributes<HTMLDivElement>) => (
79
+ <div
80
+ className={cn(
81
+ "flex flex-col space-y-2 text-center sm:text-left",
82
+ className
83
+ )}
84
+ {...props}
85
+ />
86
+ );
87
+ SheetHeader.displayName = "SheetHeader";
88
+
89
+ const SheetFooter = ({
90
+ className,
91
+ ...props
92
+ }: React.HTMLAttributes<HTMLDivElement>) => (
93
+ <div
94
+ className={cn(
95
+ "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
96
+ className
97
+ )}
98
+ {...props}
99
+ />
100
+ );
101
+ SheetFooter.displayName = "SheetFooter";
102
+
103
+ const SheetTitle = React.forwardRef<
104
+ React.ElementRef<typeof SheetPrimitive.Title>,
105
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
106
+ >(({ className, ...props }, ref) => (
107
+ <SheetPrimitive.Title
108
+ ref={ref}
109
+ className={cn("text-lg font-semibold text-foreground", className)}
110
+ {...props}
111
+ />
112
+ ));
113
+ SheetTitle.displayName = SheetPrimitive.Title.displayName;
114
+
115
+ const SheetDescription = React.forwardRef<
116
+ React.ElementRef<typeof SheetPrimitive.Description>,
117
+ React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
118
+ >(({ className, ...props }, ref) => (
119
+ <SheetPrimitive.Description
120
+ ref={ref}
121
+ className={cn("text-sm text-muted-foreground", className)}
122
+ {...props}
123
+ />
124
+ ));
125
+ SheetDescription.displayName = SheetPrimitive.Description.displayName;
126
+
127
+ export {
128
+ Sheet,
129
+ SheetClose,
130
+ SheetContent,
131
+ SheetDescription,
132
+ SheetFooter,
133
+ SheetHeader,
134
+ SheetOverlay,
135
+ SheetPortal,
136
+ SheetTitle,
137
+ SheetTrigger,
138
+ };
src/components/ui/skeleton.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.HTMLAttributes<HTMLDivElement>) {
7
+ return (
8
+ <div
9
+ className={cn("animate-pulse rounded-sm bg-primary/10", className)}
10
+ {...props}
11
+ />
12
+ )
13
+ }
14
+
15
+ export { Skeleton }
src/components/ui/sonner.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useTheme } from "next-themes"
4
+ import { Toaster as Sonner } from "sonner"
5
+
6
+ type ToasterProps = React.ComponentProps<typeof Sonner>
7
+
8
+ const Toaster = ({ ...props }: ToasterProps) => {
9
+ const { theme = "system" } = useTheme()
10
+
11
+ return (
12
+ <Sonner
13
+ theme={theme as ToasterProps["theme"]}
14
+ className="toaster group"
15
+ toastOptions={{
16
+ classNames: {
17
+ toast:
18
+ "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
19
+ description: "group-[.toast]:text-muted-foreground",
20
+ actionButton:
21
+ "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
22
+ cancelButton:
23
+ "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
24
+ },
25
+ }}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ export { Toaster }
src/components/ui/tabs.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const TooltipProvider = TooltipPrimitive.Provider
10
+
11
+ const Tooltip = TooltipPrimitive.Root
12
+
13
+ const TooltipTrigger = TooltipPrimitive.Trigger
14
+
15
+ const TooltipContent = React.forwardRef<
16
+ React.ElementRef<typeof TooltipPrimitive.Content>,
17
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
18
+ >(({ className, sideOffset = 4, ...props }, ref) => (
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-50 overflow-hidden rounded-sm bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
30
+
31
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
src/components/ui/textarea.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ export interface TextareaProps
6
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
7
+
8
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
9
+ ({ className, ...props }, ref) => {
10
+ return (
11
+ <textarea
12
+ className={cn(
13
+ "flex w-full border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
14
+ className
15
+ )}
16
+ ref={ref}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+ )
22
+ Textarea.displayName = "Textarea"
23
+
24
+ export { Textarea }
src/components/ui/tooltip.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ const TooltipProvider = TooltipPrimitive.Provider
10
+
11
+ const Tooltip = TooltipPrimitive.Root
12
+
13
+ const TooltipTrigger = TooltipPrimitive.Trigger
14
+
15
+ const TooltipContent = React.forwardRef<
16
+ React.ElementRef<typeof TooltipPrimitive.Content>,
17
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
18
+ >(({ className, sideOffset = 4, ...props }, ref) => (
19
+ <TooltipPrimitive.Content
20
+ ref={ref}
21
+ sideOffset={sideOffset}
22
+ className={cn(
23
+ "z-50 overflow-hidden rounded-sm bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ ))
29
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName
30
+
31
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
src/lib/mistral-tokenizer-js.d.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ declare module "mistral-tokenizer-js" {
2
+ export function encode(input: string): string[];
3
+ }
src/lib/token-counter.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Message } from "ai";
2
+ import mistralTokenizer from "mistral-tokenizer-js";
3
+ import { ChatCompletionMessageParam } from "openai/resources/index.mjs";
4
+
5
+ export const tokenLimit = process.env.NEXT_PUBLIC_TOKEN_LIMIT ? parseInt(process.env.NEXT_PUBLIC_TOKEN_LIMIT) : 4096;
6
+
7
+ export const encodeChat = (
8
+ messages: Message[] | ChatCompletionMessageParam[]
9
+ ): number => {
10
+ const tokensPerMessage = 3;
11
+ let numTokens = 0;
12
+ for (const message of messages) {
13
+ numTokens += tokensPerMessage;
14
+ numTokens += mistralTokenizer.encode(message.role).length;
15
+ if (typeof message.content === "string") {
16
+ numTokens += mistralTokenizer.encode(message.content).length;
17
+ }
18
+ }
19
+ numTokens += 3;
20
+ return numTokens;
21
+ };