├── .gitignore
├── LICENSE
├── README.md
├── app-icon.png
├── app
├── (chat)
│ ├── layout.tsx
│ └── page.tsx
├── action.tsx
├── agent
│ └── page.tsx
├── api
│ ├── chat
│ │ └── route.ts
│ └── retrieval
│ │ ├── chat
│ │ └── route.ts
│ │ └── ingest
│ │ └── route.ts
├── favicon.ico
├── globals.css
└── layout.tsx
├── build-server.ts
├── components.json
├── components
├── AppLayout
│ ├── index.tsx
│ └── style.css
├── HomeBlock
│ ├── index.tsx
│ └── style.css
├── agents.tsx
├── button-scroll-to-bottom.tsx
├── chat-history.tsx
├── chat-list.tsx
├── chat-message-actions.tsx
├── chat-message.tsx
├── chat-scroll-anchor.tsx
├── chat.tsx
├── clear-history.tsx
├── collapsible-message.tsx
├── copilot.tsx
├── empty-screen.tsx
├── external-link.tsx
├── followup-panel.tsx
├── footer.tsx
├── fortune.tsx
├── header.tsx
├── markdown.tsx
├── message.tsx
├── prompt-form.tsx
├── providers.tsx
├── search-related.tsx
├── search-results-image.tsx
├── search-results.tsx
├── search-skeleton.tsx
├── section.tsx
├── sidebar-actions.tsx
├── sidebar-desktop.tsx
├── sidebar-footer.tsx
├── sidebar-item.tsx
├── sidebar-items.tsx
├── sidebar-list.tsx
├── sidebar-mobile.tsx
├── sidebar-toggle.tsx
├── sidebar.tsx
├── tailwind-indicator.tsx
├── theme-toggle.tsx
├── tool-badge.tsx
├── ui
│ ├── alert-dialog.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── carousel.tsx
│ ├── checkbox.tsx
│ ├── codeblock.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── markdown.tsx
│ ├── popover.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
├── user-message.tsx
└── writer.tsx
├── lib
├── agents
│ ├── index.tsx
│ ├── inquire.tsx
│ ├── query-suggestor.tsx
│ ├── researcher.tsx
│ └── task-manager.tsx
├── hooks
│ ├── use-at-bottom.tsx
│ ├── use-copy-to-clipboard.tsx
│ ├── use-local-storage.ts
│ ├── use-setting.tsx
│ └── use-sidebar.tsx
├── schema
│ ├── inquiry.tsx
│ ├── next-action.tsx
│ ├── related.tsx
│ └── search.tsx
├── types.ts
└── utils.ts
├── next-env.d.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.cjs
├── server
├── agent.ts
├── custom
│ ├── llm
│ │ └── gemini
│ │ │ ├── chat_models.d.ts
│ │ │ ├── chat_models.js
│ │ │ ├── embeddings.d.ts
│ │ │ ├── embeddings.js
│ │ │ ├── index.d.ts
│ │ │ ├── index.js
│ │ │ ├── utils.d.ts
│ │ │ └── utils.js
│ └── tools
│ │ └── dalle
│ │ ├── dalle.d.ts
│ │ └── dalle.js
├── server.ts
└── tsconfig.json
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ └── main.rs
└── tauri.conf.json
├── tailwind.config.js
├── tailwind.config.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | */.env
133 | .env
134 | /build
135 | /src-tauri/target/
136 | /src-tauri/bin/
137 | .DS_Store
138 | /server/*.js
139 | .vercel
140 | .env*.local
141 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Frank Lin
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next-Langchain-Tauri
2 |
3 | Next-Langchain-Tauri is a desktop application that combines the power of Next.js for frontend development, Langchain.js for AI processing, and Tauri for packaging the application as a desktop app.
4 |
5 |
6 |
7 | ## How to use
8 |
9 | Click the ``` Key Setting``` button to enter API keys, which are only stored in your computer, and then start to chat.
10 |
11 |
12 |
13 | ## Development
14 |
15 | 1. Ensure you have Node.js v20, npm, rust, and cargo installed on your system.
16 |
17 | 2. Installation
18 | ``` bash
19 | pnpm install
20 | ```
21 |
22 | 3. use [yao-pkg](https://github.com/yao-pkg/pkg-binaries) to pack the server into a single executable file and make it as a sidecar binary for Tauri, before packing you need to check your computer arch by running:
23 | ``` bash
24 | rustc -Vv | grep host | cut -f2 -d' '
25 | ```
26 | then change the word ```server-aarch64-apple-darwin``` to ```server-yours``` in packages.json, for example ```server-x86_64-apple-darwin```
27 |
28 | save and run:
29 | ``` bash
30 | pnpm install -g @yao-pkg/pkg
31 | pnpm pkg-server
32 | ```
33 |
34 | 4. Change 'next.config.mjs' to the part for tarui
35 |
36 |
37 | 5. Build
38 | ``` bash
39 | pnpm tauri build
40 | ```
41 |
42 | ## Credits
43 |
44 | This project was inspired by and incorporates code from the following repositories:
45 |
46 | - [Vercel/ai-chatbot](https://github.com/vercel/ai-chatbot)
47 | - [langchain-ai/langchain-nextjs-template](https://github.com/langchain-ai/langchain-nextjs-template)
48 | - [srsholmes/tauri-nextjs-api-routes](https://github.com/srsholmes/tauri-nextjs-api-routes)
49 |
50 |
--------------------------------------------------------------------------------
/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/app-icon.png
--------------------------------------------------------------------------------
/app/(chat)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { SidebarDesktop } from '@/components/sidebar-desktop'
2 |
3 | interface ChatLayoutProps {
4 | children: React.ReactNode
5 | }
6 |
7 | export default async function ChatLayout({ children }: ChatLayoutProps) {
8 | return (
9 |
10 |
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/app/(chat)/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Chat } from '@/components/chat'
3 | import { useSearchParams } from 'next/navigation'
4 | export default function IndexPage() {
5 | const params = useSearchParams()
6 | const timestamp = `${new Date().toISOString().split('.')[0]}`
7 | return
8 | }
9 |
--------------------------------------------------------------------------------
/app/action.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | StreamableValue,
3 | createAI,
4 | createStreamableUI,
5 | createStreamableValue,
6 | getMutableAIState
7 | } from 'ai/rsc'
8 | import { ExperimentalMessage } from 'ai'
9 | import { IconSpinner} from '@/components/ui/icons'
10 |
11 | import { Section } from '@/components/section'
12 | import { FollowupPanel } from '@/components/followup-panel'
13 | import { inquire, researcher, taskManager, querySuggestor } from '@/lib/agents'
14 |
15 | async function submit(formData?: FormData, skip?: boolean,apikeys?:any,) {
16 | 'use server'
17 | const aiState = getMutableAIState()
18 | const uiStream = createStreamableUI()
19 | const isGenerating = createStreamableValue(true)
20 | const isCollapsed = createStreamableValue(false)
21 |
22 | const messages: ExperimentalMessage[] = aiState.get() as any
23 | console.log(formData)
24 | // Limit the number of messages to 10
25 | messages.splice(0, Math.max(messages.length - 10, 0))
26 | // Get the user input from the form data
27 | const userInput = skip
28 | ? `{"action": "skip"}`
29 | : (formData?.get('input') as string)
30 | const content = skip
31 | ? userInput
32 | : formData
33 | ? JSON.stringify(Object.fromEntries(formData))
34 | : null
35 | // Add the user message to the state
36 | if (content) {
37 | const message = { role: 'user', content }
38 | messages.push(message as ExperimentalMessage)
39 | aiState.update([...(aiState.get() as any), message])
40 | }
41 |
42 | async function processEvents() {
43 | uiStream.update()
44 | let action: any = { object: { next: 'proceed' } }
45 | // If the user skips the task, we proceed to the search
46 | if (!skip) action = (await taskManager(messages,apikeys?.llm_base_url as string,apikeys?.llm_api_key as string,apikeys?.llm_model as string)) ?? action
47 | if (action.object.next === 'inquire') {
48 |
49 | // Generate inquiry
50 | const inquiry = await inquire(uiStream, messages,apikeys?.llm_base_url as string,apikeys?.llm_api_key as string,apikeys?.llm_model as string)
51 |
52 | uiStream.done()
53 | isGenerating.done()
54 | isCollapsed.done(false)
55 | aiState.done([
56 | ...aiState.get(),
57 | { role: 'assistant', content: `inquiry: ${inquiry?.question}` }
58 | ])
59 | return
60 | }
61 | // Set the collapsed state to true
62 | isCollapsed.done(true)
63 |
64 | // Generate the answer
65 | let answer = ''
66 | let errorOccurred = false
67 | const streamText = createStreamableValue()
68 | while (answer.length === 0) {
69 | // Search the web and generate the answer
70 | const { fullResponse, hasError } = await researcher(
71 | uiStream,
72 | streamText,
73 | messages,
74 | apikeys?.llm_base_url as string,
75 | apikeys?.llm_api_key as string,
76 | apikeys?.llm_model as string,
77 | apikeys?.tavilyserp_api_key as string
78 | )
79 | answer = fullResponse
80 | errorOccurred = hasError
81 | }
82 | streamText.done()
83 |
84 | if (!errorOccurred) {
85 | // Generate related queries
86 | await querySuggestor(uiStream, messages,apikeys?.llm_base_url as string,apikeys?.llm_api_key as string,apikeys?.llm_model as string)
87 |
88 | // Add follow-up panel
89 | uiStream.append(
90 |
93 | )
94 | }
95 |
96 | isGenerating.done(false)
97 | uiStream.done()
98 | aiState.done([...aiState.get(), { role: 'assistant', content: answer }])
99 | }
100 |
101 | processEvents()
102 |
103 | return {
104 | id: Date.now(),
105 | isGenerating: isGenerating.value,
106 | component: uiStream.value,
107 | isCollapsed: isCollapsed.value
108 | }
109 | }
110 |
111 | // Define the initial state of the AI. It can be any JSON object.
112 | const initialAIState: {
113 | role: 'user' | 'assistant' | 'system' | 'function' | 'tool'
114 | content: string
115 | id?: string
116 | name?: string
117 | }[] = []
118 |
119 | // The initial UI state that the client will keep track of, which contains the message IDs and their UI nodes.
120 | const initialUIState: {
121 | id: number
122 | isGenerating?: StreamableValue
123 | isCollapsed?: StreamableValue
124 | component: React.ReactNode
125 | }[] = []
126 |
127 | // AI is a provider you wrap your application with so you can access AI and UI state in your components.
128 | export const AI = createAI({
129 | actions: {
130 | submit
131 | },
132 | // Each state can be any shape of object, but for chat applications
133 | // it makes sense to have an array of messages. Or you may prefer something like { id: number, messages: Message[] }
134 | initialUIState,
135 | initialAIState
136 | })
137 |
--------------------------------------------------------------------------------
/app/agent/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Agents from '@/components/agents'
3 | import { useState } from 'react'
4 |
5 | export default function AgentsPage() {
6 | const [showPinnedOnly, setShowPinnedOnly] = useState(false);
7 | return
8 | }
9 |
--------------------------------------------------------------------------------
/app/api/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { Chat } from '@/server/agent'
2 | // export const runtime = 'edge';
3 | // edge conflicts with duckduckgo-scape
4 | export async function POST(req: Request) {
5 | const json = await req.json()
6 | return Chat(json)
7 | }
--------------------------------------------------------------------------------
/app/api/retrieval/chat/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { Message as VercelChatMessage, StreamingTextResponse } from "ai";
3 |
4 | import { createClient } from "@supabase/supabase-js";
5 |
6 | import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
7 | import { PromptTemplate } from "@langchain/core/prompts";
8 | import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
9 | import { Document } from "@langchain/core/documents";
10 | import { RunnableSequence } from "@langchain/core/runnables";
11 | import {
12 | BytesOutputParser,
13 | StringOutputParser,
14 | } from "@langchain/core/output_parsers";
15 |
16 | export const runtime = "edge";
17 |
18 | const combineDocumentsFn = (docs: Document[]) => {
19 | const serializedDocs = docs.map((doc) => doc.pageContent);
20 | return serializedDocs.join("\n\n");
21 | };
22 |
23 | const formatVercelMessages = (chatHistory: VercelChatMessage[]) => {
24 | const formattedDialogueTurns = chatHistory.map((message) => {
25 | if (message.role === "user") {
26 | return `Human: ${message.content}`;
27 | } else if (message.role === "assistant") {
28 | return `Assistant: ${message.content}`;
29 | } else {
30 | return `${message.role}: ${message.content}`;
31 | }
32 | });
33 | return formattedDialogueTurns.join("\n");
34 | };
35 |
36 | const CONDENSE_QUESTION_TEMPLATE = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.
37 |
38 |
39 | {chat_history}
40 |
41 |
42 | Follow Up Input: {question}
43 | Standalone question:`;
44 | const condenseQuestionPrompt = PromptTemplate.fromTemplate(
45 | CONDENSE_QUESTION_TEMPLATE,
46 | );
47 |
48 | const ANSWER_TEMPLATE = `
49 |
50 | {context}
51 |
52 |
53 |
54 | {chat_history}
55 |
56 |
57 | Question: {question}
58 | `;
59 | const answerPrompt = PromptTemplate.fromTemplate(ANSWER_TEMPLATE);
60 |
61 | /**
62 | * This handler initializes and calls a retrieval chain. It composes the chain using
63 | * LangChain Expression Language. See the docs for more information:
64 | *
65 | * https://js.langchain.com/docs/guides/expression_language/cookbook#conversational-retrieval-chain
66 | */
67 | export async function POST(req: NextRequest) {
68 | try {
69 | const body = await req.json();
70 | const messages = body.messages ?? [];
71 | const previousMessages = messages.slice(0, -1);
72 | const currentMessageContent = messages[messages.length - 1].content;
73 |
74 | const model = new ChatOpenAI({
75 | temperature: 0.7,
76 | modelName: (body.annotations?body.annotations[0]:undefined) || body.previewToken.llm_model || 'gpt-3.5-turbo-0125',
77 | openAIApiKey: body.previewToken.llm_api_key,
78 | configuration: { baseURL:body.previewToken.llm_base_url },
79 | maxTokens: 4096,
80 | streaming: true
81 | });
82 |
83 | const client = createClient(
84 | process.env.SUPABASE_URL!,
85 | process.env.SUPABASE_PRIVATE_KEY!,
86 | );
87 | const vectorstore = new SupabaseVectorStore(new OpenAIEmbeddings({openAIApiKey:body.previewToken.llm_api_key,configuration: { baseURL:body.previewToken.llm_base_url }}), {
88 | client,
89 | tableName: "documents",
90 | queryName: "match_documents",
91 | });
92 |
93 | /**
94 | * We use LangChain Expression Language to compose two chains.
95 | * To learn more, see the guide here:
96 | *
97 | * https://js.langchain.com/docs/guides/expression_language/cookbook
98 | *
99 | * You can also use the "createRetrievalChain" method with a
100 | * "historyAwareRetriever" to get something prebaked.
101 | */
102 | const standaloneQuestionChain = RunnableSequence.from([
103 | condenseQuestionPrompt,
104 | model,
105 | new StringOutputParser(),
106 | ]);
107 |
108 | let resolveWithDocuments: (value: Document[]) => void;
109 | const documentPromise = new Promise((resolve) => {
110 | resolveWithDocuments = resolve;
111 | });
112 |
113 | const retriever = vectorstore.asRetriever({
114 | callbacks: [
115 | {
116 | handleRetrieverEnd(documents) {
117 | resolveWithDocuments(documents);
118 | },
119 | },
120 | ],
121 | });
122 |
123 | const retrievalChain = retriever.pipe(combineDocumentsFn);
124 |
125 | const answerChain = RunnableSequence.from([
126 | {
127 | context: RunnableSequence.from([
128 | (input) => input.question,
129 | retrievalChain,
130 | ]),
131 | chat_history: (input) => input.chat_history,
132 | question: (input) => input.question,
133 | },
134 | answerPrompt,
135 | model,
136 | ]);
137 |
138 | const conversationalRetrievalQAChain = RunnableSequence.from([
139 | {
140 | question: standaloneQuestionChain,
141 | chat_history: (input) => input.chat_history,
142 | },
143 | answerChain,
144 | new BytesOutputParser(),
145 | ]);
146 |
147 | const stream = await conversationalRetrievalQAChain.stream({
148 | question: currentMessageContent,
149 | chat_history: formatVercelMessages(previousMessages),
150 | });
151 |
152 | const documents = await documentPromise;
153 | const serializedSources = Buffer.from(
154 | JSON.stringify(
155 | documents.map((doc) => {
156 | return {
157 | pageContent: doc.pageContent.slice(0, 50) + "...",
158 | metadata: doc.metadata,
159 | };
160 | }),
161 | ),
162 | ).toString("base64");
163 |
164 | return new StreamingTextResponse(stream, {
165 | headers: {
166 | "x-message-index": (previousMessages.length + 1).toString(),
167 | "x-sources": serializedSources,
168 | },
169 | });
170 | } catch (e: any) {
171 | console.log(e);
172 | return NextResponse.json({ error: e.message }, { status: e.status ?? 500 });
173 | }
174 | }
--------------------------------------------------------------------------------
/app/api/retrieval/ingest/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
3 |
4 | import { createClient } from "@supabase/supabase-js";
5 | import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
6 | import { OpenAIEmbeddings } from "@langchain/openai";
7 |
8 |
9 | export const runtime = "edge";
10 |
11 | // Before running, follow set-up instructions at
12 | // https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/supabase
13 |
14 | /**
15 | * This handler takes input text, splits it into chunks, and embeds those chunks
16 | * into a vector store for later retrieval. See the following docs for more information:
17 | *
18 | * https://js.langchain.com/docs/modules/data_connection/document_transformers/text_splitters/recursive_text_splitter
19 | * https://js.langchain.com/docs/modules/data_connection/vectorstores/integrations/supabase
20 | */
21 | export async function POST(req: NextRequest) {
22 | const body = await req.json();
23 | const text = body.text;
24 |
25 | if (process.env.NEXT_PUBLIC_DEMO === "true") {
26 | return NextResponse.json(
27 | {
28 | error: [
29 | "Ingest is not supported in demo mode.",
30 | "Please set up your own version of the repo here: https://github.com/langchain-ai/langchain-nextjs-template",
31 | ].join("\n"),
32 | },
33 | { status: 403 },
34 | );
35 | }
36 |
37 | try {
38 | const client = createClient(
39 | process.env.SUPABASE_URL!,
40 | process.env.SUPABASE_PRIVATE_KEY!,
41 | );
42 |
43 | const splitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
44 | chunkSize: 256,
45 | chunkOverlap: 20,
46 | });
47 |
48 | const splitDocuments = await splitter.createDocuments([text]);
49 |
50 | const vectorstore = await SupabaseVectorStore.fromDocuments(
51 | splitDocuments,
52 | new OpenAIEmbeddings( {openAIApiKey:body.llm_api_key,configuration: { baseURL:body.llm_base_url }}),
53 | {
54 | client,
55 | tableName: "documents",
56 | queryName: "match_documents",
57 | },
58 | );
59 |
60 | return NextResponse.json({ ok: true }, { status: 200 });
61 | } catch (e: any) {
62 | return NextResponse.json({ error: e.message }, { status: 500 });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
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: 0.5rem;
28 | }
29 |
30 | .dark {
31 | --background: 240 10% 3.9%;
32 | --foreground: 0 0% 98%;
33 | --card: 240 10% 3.9%;
34 | --card-foreground: 0 0% 98%;
35 | --popover: 240 10% 3.9%;
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 |
54 | @layer base {
55 | * {
56 | @apply border-border;
57 | }
58 | body {
59 | @apply bg-background text-foreground;
60 | }
61 | }
62 |
63 | .title {
64 | background: linear-gradient(to right, orange,#10b981);
65 | -webkit-background-clip: text;
66 | color: transparent;
67 | display: inline-block;
68 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from 'react-hot-toast'
2 | import { GeistSans } from 'geist/font/sans'
3 | import { GeistMono } from 'geist/font/mono'
4 |
5 | import '@/app/globals.css'
6 | import { cn } from '@/lib/utils'
7 | import { Providers } from '@/components/providers'
8 | import Header from '@/components/header'
9 |
10 | export const metadata = {
11 | metadataBase: new URL(`http://${process.env.VERCEL_URL}`),
12 | title: {
13 | default: 'Next.js AI Chatbot',
14 | template: `%s - Next.js AI Chatbot`
15 | },
16 | description: 'An AI-powered chatbot template built with Next.js and Vercel.',
17 | icons: {
18 | icon: '/favicon.ico',
19 | shortcut: '/favicon-16x16.png',
20 | apple: '/apple-touch-icon.png'
21 | }
22 | }
23 |
24 | export const viewport = {
25 | themeColor: [
26 | { media: '(prefers-color-scheme: light)', color: 'white' },
27 | { media: '(prefers-color-scheme: dark)', color: 'black' }
28 | ]
29 | }
30 |
31 | interface RootLayoutProps {
32 | children: React.ReactNode
33 | }
34 |
35 | export default function RootLayout({ children }: RootLayoutProps) {
36 | return (
37 |
38 |
45 |
46 |
52 |
53 |
54 | {children}
55 |
56 | {/* */}
57 |
58 |
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/build-server.ts:
--------------------------------------------------------------------------------
1 | import * as esbuild from 'esbuild';
2 |
3 | (async () => {
4 | await esbuild.build({
5 | entryPoints: ['./server/server.ts'],
6 | bundle: true,
7 | platform: 'node',
8 | target: ['node20.0'],
9 | outfile: 'build/server.js',
10 | plugins: [],
11 | });
12 | })();
13 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/AppLayout/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type AppLayoutProps = {
4 | children?: React.ReactNode;
5 | }
6 |
7 | export default function AppLayout(props: AppLayoutProps) {
8 | return (
9 |
10 | {props.children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/AppLayout/style.css:
--------------------------------------------------------------------------------
1 | .app-layout {
2 | display: flex;
3 | flex: 1;
4 | width: 100%;
5 | height: 100%;
6 | }
7 |
--------------------------------------------------------------------------------
/components/HomeBlock/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type HomeBlockProps = {
4 | url: string,
5 | title: string,
6 | description: string,
7 | };
8 |
9 | const HomeBlock = (props: HomeBlockProps) => (
10 |
11 |
12 |
📚 {props.title}
13 |
{props.description}
14 |
15 |
16 | )
17 |
18 | export default HomeBlock;
19 |
--------------------------------------------------------------------------------
/components/HomeBlock/style.css:
--------------------------------------------------------------------------------
1 | .home-block-anchor {
2 | text-decoration: none;
3 | border-radius: 8px;
4 | border: 1px solid #E9EBEE;
5 | padding: 0px 8px;
6 | width: 200px;
7 | height: fit-content;
8 | color: black;
9 | }
10 |
11 | .home-block-anchor:hover {
12 | transform: scale(1.1);
13 | background-color: #E5E7EB;
14 | }
15 |
16 | .home-block {
17 | display: flex;
18 | flex-direction: column;
19 | }
20 |
21 | .home-block-title {
22 | font-size: 18px;
23 | margin: 8px 0px 0px 0px;
24 | }
25 |
26 | .home-block-description {
27 | font-size: 16px;
28 | }
29 |
--------------------------------------------------------------------------------
/components/button-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconArrowDown } from '@/components/ui/icons'
9 |
10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11 | const isAtBottom = useAtBottom()
12 |
13 | return (
14 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/chat-history.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import Link from 'next/link'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { SidebarList } from '@/components/sidebar-list'
7 | import { buttonVariants } from '@/components/ui/button'
8 | import { IconPlus } from '@/components/ui/icons'
9 |
10 | interface ChatHistoryProps {
11 | userId?: string
12 | }
13 |
14 | export function ChatHistory({ userId }: ChatHistoryProps) {
15 | return (
16 |
17 |
20 | {Array.from({ length: 10 }).map((_, i) => (
21 |
25 | ))}
26 |
27 | }
28 | >
29 | {/* @ts-ignore */}
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/components/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | import { Separator } from '@/components/ui/separator'
4 | import { ChatMessage } from '@/components/chat-message'
5 |
6 | export interface ChatList {
7 | messages: Message[]
8 | }
9 |
10 | export function ChatList({ messages }: ChatList) {
11 | // if (!messages.length) {
12 | // return null
13 | // }
14 |
15 | return (
16 |
17 | {messages.map((message, index) => (
18 |
19 |
20 | {index < messages.length - 1 && (
21 |
22 | )}
23 |
24 | ))}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3 |
4 | import { Message } from 'ai'
5 | import remarkGfm from 'remark-gfm'
6 | import remarkMath from 'remark-math'
7 |
8 | import { cn } from '@/lib/utils'
9 | import { CodeBlock } from '@/components/ui/codeblock'
10 | import { MemoizedReactMarkdown } from '@/components/markdown'
11 | import { IconOpenAI, IconUser } from '@/components/ui/icons'
12 | import { ChatMessageActions } from '@/components/chat-message-actions'
13 | import rehypeExternalLinks from "rehype-external-links";
14 |
15 | export interface ChatMessageProps {
16 | message: Message
17 | }
18 |
19 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
20 | return (
21 |
25 |
33 | {message.role === 'user' ? : }
34 |
35 |
36 | {children}
43 | },
44 | code({children, className, node, ...props }) {
45 | if(node?.children[0]){
46 | const child = node.children[0]
47 | if (child.data == '▍') {
48 | return (
49 | ▍
50 | )
51 | }
52 | }
53 |
54 | const match = /language-(\w+)/.exec(className || '')
55 |
56 | if(node?.properties.inline === true) {
57 | return (
58 |
59 | {children}
60 |
61 | )
62 | }
63 |
64 | return (
65 |
71 | )
72 | }
73 | }}
74 | >
75 | {message.content}
76 |
77 |
78 |
79 |
80 | )
81 | }
--------------------------------------------------------------------------------
/components/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useInView } from 'react-intersection-observer'
5 |
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 |
8 | interface ChatScrollAnchorProps {
9 | trackVisibility?: boolean
10 | }
11 |
12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
13 | const isAtBottom = useAtBottom()
14 | const { ref, entry, inView } = useInView({
15 | trackVisibility,
16 | delay: 100,
17 | rootMargin: '0px 0px -150px 0px'
18 | })
19 |
20 | React.useEffect(() => {
21 | if (isAtBottom && trackVisibility && !inView) {
22 | entry?.target.scrollIntoView({
23 | block: 'start'
24 | })
25 | }
26 | }, [inView, entry, isAtBottom, trackVisibility])
27 |
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { ServerActionResult } from '@/lib/types'
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle,
18 | AlertDialogTrigger
19 | } from '@/components/ui/alert-dialog'
20 | import { IconSpinner } from '@/components/ui/icons'
21 |
22 | interface ClearHistoryProps {
23 | isEnabled: boolean
24 | }
25 |
26 | export function ClearHistory({
27 | isEnabled = false
28 | }: ClearHistoryProps) {
29 | const [open, setOpen] = React.useState(false)
30 | const [isPending, startTransition] = React.useTransition()
31 | const router = useRouter()
32 |
33 | return (
34 |
35 |
36 |
40 |
41 |
42 |
43 | Are you absolutely sure?
44 |
45 | This will permanently delete your chat history and remove your data
46 | from our servers.
47 |
48 |
49 |
50 | Cancel
51 | {
54 | event.preventDefault()
55 | startTransition(() => {
56 | const chats = Object.keys(localStorage).filter(key => key.startsWith('cid_'));
57 | chats.forEach(key => {
58 | localStorage.removeItem(key);
59 | });
60 | setOpen(false)
61 | router.replace('/')
62 | router.refresh()
63 | toast.success('Chat deleted')
64 | })
65 | }}
66 | >
67 | {isPending && }
68 | Delete
69 |
70 |
71 |
72 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/components/collapsible-message.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import {
3 | Collapsible,
4 | CollapsibleTrigger,
5 | CollapsibleContent
6 | } from '@radix-ui/react-collapsible'
7 | import { Button } from './ui/button'
8 | import { ChevronDown } from 'lucide-react'
9 | import { StreamableValue, useStreamableValue } from 'ai/rsc'
10 | import { cn } from '@/lib/utils'
11 | import { Separator } from './ui/separator'
12 |
13 | interface CollapsibleMessageProps {
14 | message: {
15 | id: number
16 | isCollapsed?: StreamableValue
17 | component: React.ReactNode
18 | }
19 | isLastMessage?: boolean
20 | }
21 |
22 | export const CollapsibleMessage: React.FC = ({
23 | message,
24 | isLastMessage = false
25 | }) => {
26 | const [data] = useStreamableValue(message.isCollapsed)
27 | const isCollapsed = data ?? false
28 | const [open, setOpen] = useState(isLastMessage)
29 |
30 | useEffect(() => {
31 | setOpen(isLastMessage)
32 | }, [isCollapsed, isLastMessage])
33 |
34 | // if not collapsed, return the component
35 | if (!isCollapsed) {
36 | return message.component
37 | }
38 |
39 | return (
40 | {
43 | setOpen(value)
44 | }}
45 | >
46 |
47 |
53 |
66 |
67 |
68 | {message.component}
69 | {!open && }
70 |
71 | )
72 | }
--------------------------------------------------------------------------------
/components/copilot.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useEffect, useState } from 'react'
4 | import { PartialInquiry } from '@/lib/schema/inquiry'
5 | import { Input } from './ui/input'
6 | import { Checkbox } from './ui/checkbox'
7 | import { Button } from './ui/button'
8 | import { Card } from './ui/card'
9 | import { ArrowRight, Check, FastForward, Sparkles } from 'lucide-react'
10 | import { useActions, useStreamableValue, useUIState } from 'ai/rsc'
11 | import { AI } from '@/app/action'
12 | import { cn } from '@/lib/utils'
13 | import {useSetting} from '@/lib/hooks/use-setting'
14 |
15 | export type CopilotProps = {
16 | inquiry?: PartialInquiry
17 | }
18 |
19 | export const Copilot: React.FC = ({ inquiry }: CopilotProps) => {
20 | const [completed, setCompleted] = useState(false)
21 | const [query, setQuery] = useState('')
22 | const [skipped, setSkipped] = useState(false)
23 | const [data, error, pending] = useStreamableValue(inquiry)
24 | const [checkedOptions, setCheckedOptions] = useState<{
25 | [key: string]: boolean
26 | }>({})
27 | const [isButtonDisabled, setIsButtonDisabled] = useState(true)
28 | const [messages, setMessages] = useUIState()
29 | const { submit } = useActions()
30 | const [keys, setKeys] = useSetting();
31 | const handleInputChange = (event: React.ChangeEvent) => {
32 | setQuery(event.target.value)
33 | checkIfButtonShouldBeEnabled()
34 | }
35 |
36 | const handleOptionChange = (selectedOption: string) => {
37 | const updatedCheckedOptions = {
38 | ...checkedOptions,
39 | [selectedOption]: !checkedOptions[selectedOption]
40 | }
41 | setCheckedOptions(updatedCheckedOptions)
42 | checkIfButtonShouldBeEnabled(updatedCheckedOptions)
43 | }
44 |
45 | const checkIfButtonShouldBeEnabled = (currentOptions = checkedOptions) => {
46 | const anyCheckboxChecked = Object.values(currentOptions).some(
47 | checked => checked
48 | )
49 | setIsButtonDisabled(!(anyCheckboxChecked || query))
50 | }
51 |
52 | const updatedQuery = () => {
53 | const selectedOptions = Object.entries(checkedOptions)
54 | .filter(([, checked]) => checked)
55 | .map(([option]) => option)
56 | return [...selectedOptions, query].filter(Boolean).join(', ')
57 | }
58 |
59 | useEffect(() => {
60 | checkIfButtonShouldBeEnabled()
61 | // eslint-disable-next-line react-hooks/exhaustive-deps
62 | }, [query])
63 |
64 | const onFormSubmit = async (
65 | e: React.FormEvent,
66 | skip?: boolean
67 | ) => {
68 | e.preventDefault()
69 | setCompleted(true)
70 | setSkipped(skip || false)
71 |
72 | const formData = skip
73 | ? undefined
74 | : new FormData(e.target as HTMLFormElement)
75 |
76 | const apikeys = {'llm_api_key':keys.current.llm_api_key,'llm_base_url':keys.current.llm_base_url,'llm_model':keys.current.llm_model,'tavilyserp_api_key':keys.current.tavilyserp_api_key}
77 | const responseMessage = await submit(formData, skip,apikeys)
78 | setMessages(currentMessages => [...currentMessages, responseMessage])
79 | }
80 |
81 | const handleSkip = (e: React.MouseEvent) => {
82 | onFormSubmit(e as unknown as React.FormEvent, true)
83 | }
84 |
85 | if (error) {
86 | return (
87 |
88 |
89 |
90 |
91 | {`error: ${error}`}
92 |
93 |
94 |
95 | )
96 | }
97 |
98 | if (skipped) {
99 | return null
100 | }
101 |
102 | if (completed) {
103 | return (
104 |
105 |
106 |
107 | {updatedQuery()}
108 |
109 |
110 |
111 |
112 | )
113 | } else {
114 | return (
115 |
116 |
117 |
118 |
119 | {data?.question}
120 |
121 |
122 |
177 |
178 | )
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 | import { ArrowRight } from 'lucide-react'
3 |
4 | const exampleMessages = [
5 | {
6 | heading: 'Why is Nvidia growing rapidly?',
7 | message: 'Why is Nvidia growing rapidly?'
8 | },
9 | {
10 | heading: 'Is the Apple Vision Pro worth buying?',
11 | message: 'Is the Apple Vision Pro worth buying?'
12 | },
13 | {
14 | heading: 'How does the Vercel AI SDK work?',
15 | message: 'How does the Vercel AI SDK work?'
16 | },
17 | {
18 | heading: 'Tesla vs Rivian',
19 | message: 'Tesla vs Rivian'
20 | }
21 | ]
22 | export function EmptyScreen({
23 | submitMessage,
24 | className
25 | }: {
26 | submitMessage: (message: string) => void
27 | className?: string
28 | }) {
29 | return (
30 |
31 |
32 |
33 | {exampleMessages.map((message, index) => (
34 |
46 | ))}
47 |
48 |
49 |
50 | )
51 | }
--------------------------------------------------------------------------------
/components/external-link.tsx:
--------------------------------------------------------------------------------
1 | export function ExternalLink({
2 | href,
3 | children
4 | }: {
5 | href: string
6 | children: React.ReactNode
7 | }) {
8 | return (
9 |
14 | {children}
15 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/components/followup-panel.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 | import { Button } from './ui/button'
5 | import { Input } from './ui/input'
6 | import { useActions, useUIState } from 'ai/rsc'
7 | import type { AI } from '@/app/action'
8 | import { UserMessage } from './user-message'
9 | import { ArrowRight } from 'lucide-react'
10 | import {useSetting} from '@/lib/hooks/use-setting'
11 |
12 | export function FollowupPanel() {
13 | const [keys, setKeys] = useSetting();
14 | const [input, setInput] = useState('')
15 | const { submit } = useActions()
16 | const [, setMessages] = useUIState()
17 |
18 | const handleSubmit = async (event: React.FormEvent) => {
19 | event.preventDefault()
20 | const formData = new FormData(event.currentTarget as HTMLFormElement)
21 |
22 | const userMessage = {
23 | id: Date.now(),
24 | isGenerating: false,
25 | component:
26 | }
27 | const apikeys = {'llm_api_key':keys.current.llm_api_key,'llm_base_url':keys.current.llm_base_url,'llm_model':keys.current.llm_model,'tavilyserp_api_key':keys.current.tavilyserp_api_key}
28 | const skip=undefined
29 | const responseMessage = await submit(formData,skip,apikeys)
30 | setMessages(currentMessages => [
31 | ...currentMessages,
32 | userMessage,
33 | responseMessage
34 | ])
35 |
36 | setInput('')
37 | }
38 |
39 | return (
40 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 | import { ExternalLink } from '@/components/external-link'
5 |
6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) {
7 | return (
8 |
15 | Input @ to mention agents.
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/components/fortune.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useState, useRef } from 'react'
3 | import type { AI } from '@/app/action'
4 | import {StreamableValue, useUIState, useActions, useAIState } from 'ai/rsc'
5 | import { cn } from '@/lib/utils'
6 | import { UserMessage } from './user-message'
7 | import { Input } from './ui/input'
8 | import { Button } from './ui/button'
9 | import { ArrowRight, Plus, Square } from 'lucide-react'
10 | import { EmptyScreen } from './empty-screen'
11 | import {useSetting} from '@/lib/hooks/use-setting'
12 | import { CollapsibleMessage } from './collapsible-message'
13 |
14 | export function Fortune() {
15 | const [input, setInput] = useState('')
16 | const [messages, setMessages] = useUIState()
17 | const [aiMessages, setAiMessages] = useAIState()
18 | const { submit } = useActions()
19 | const [isButtonPressed, setIsButtonPressed] = useState(false)
20 | const inputRef = useRef(null)
21 | const [showEmptyScreen, setShowEmptyScreen] = useState(false)
22 | const [keys, setKeys] = useSetting();
23 | // Focus on input when button is pressed
24 | useEffect(() => {
25 | if (isButtonPressed) {
26 | inputRef.current?.focus()
27 | setIsButtonPressed(false)
28 | }
29 | }, [isButtonPressed])
30 |
31 | const handleSubmit = async (e: React.FormEvent) => {
32 | e.preventDefault()
33 |
34 | // Clear messages if button is pressed
35 | if (isButtonPressed) {
36 | handleClear()
37 | setIsButtonPressed(false)
38 | }
39 |
40 | // Add user message to UI state
41 | setMessages(currentMessages => [
42 | ...currentMessages,
43 | {
44 | id: Date.now(),
45 | component:
46 | }
47 | ])
48 |
49 | // Submit and get response message
50 | const formData = new FormData(e.currentTarget)
51 | const apikeys = {'llm_api_key':keys.current.llm_api_key,'llm_base_url':keys.current.llm_base_url,'llm_model':keys.current.llm_model,'tavilyserp_api_key':keys.current.tavilyserp_api_key}
52 | var skip=undefined
53 | const responseMessage = await submit(formData,skip,apikeys)
54 | setMessages(currentMessages => [...currentMessages, responseMessage as any])
55 |
56 | setInput('')
57 | }
58 |
59 | // Clear messages
60 | const handleClear = () => {
61 | setIsButtonPressed(true)
62 | setMessages([])
63 | setAiMessages([])
64 | }
65 |
66 | useEffect(() => {
67 | // focus on input when the page loads
68 | inputRef.current?.focus()
69 | }, [])
70 |
71 | // If there are messages and the new button has not been pressed, display the new Button
72 | if (messages.length > 0 && !isButtonPressed) {
73 | return (
74 |
75 | {messages.map(
76 | (message: {
77 | id: number
78 | component: React.ReactNode
79 | isCollapsed?: StreamableValue
80 | }) => (
81 |
86 | )
87 | )}
88 |
89 |
100 |
101 |
102 | )
103 | }
104 |
105 | // Condition 1 and 3: If there are no messages or the button is pressed, display the form
106 | const formPositionClass =
107 | messages.length === 0
108 | ? 'fixed bottom-8 left-0 right-0 top-10 mx-auto h-screen flex flex-col items-center justify-center'
109 | : 'fixed bottom-8-ml-6'
110 | return (
111 |
112 | {/*
*/}
113 |
146 |
147 | )
148 | }
149 |
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react'
2 | import ReactMarkdown, { Options } from 'react-markdown'
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { StreamableValue, useStreamableValue } from 'ai/rsc'
4 | import { MemoizedReactMarkdown } from './ui/markdown'
5 |
6 | export function BotMessage({
7 | content
8 | }: {
9 | content: string | StreamableValue
10 | }) {
11 | const [data, error, pending] = useStreamableValue(content)
12 |
13 | // Currently, sometimes error occurs after finishing the stream.
14 | if (error) return Error
15 |
16 | return (
17 |
18 | {data}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | import Textarea from 'react-textarea-autosize'
2 | import { UseChatHelpers } from 'ai/react'
3 | import { cn } from '@/lib/utils'
4 | import { Button, buttonVariants } from '@/components/ui/button'
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipTrigger
9 | } from '@/components/ui/tooltip'
10 | import { IconArrowElbow,IconPlus } from '@/components/ui/icons'
11 | import { useRouter } from 'next/navigation'
12 | import { useState, useRef, useEffect } from 'react'
13 |
14 | import { FooterText } from '@/components/footer'
15 | import { Agent } from '@/components/agents'
16 | import {
17 | Command,
18 | CommandGroup,
19 | CommandItem,
20 | } from "@/components/ui/command"
21 |
22 | export interface PromptProps
23 | extends Pick {
24 | onSubmit: (value: string) => void
25 | isLoading: boolean,
26 | agents: any,
27 | }
28 | export function PromptForm({
29 | onSubmit,
30 | input,
31 | setInput,
32 | isLoading,
33 | agents,
34 | }: PromptProps) {
35 | const formRef = useRef(null);
36 | const inputRef = useRef(null)
37 |
38 | const router = useRouter()
39 |
40 | const [showPopup, setshowPopup] = useState(false);
41 | const onKeyDown = (e: React.KeyboardEvent) => {
42 | if (e.key === 'Enter' && !e.shiftKey) {
43 | e.preventDefault();
44 | if (!input?.trim()) {
45 | return;
46 | }
47 | setInput('');
48 | formRef.current?.requestSubmit()
49 | } else if (e.key === 'Enter' && e.shiftKey) {
50 | setInput(input + '\n');
51 | }
52 | };
53 |
54 | const handleInputChange = (e: React.ChangeEvent) => {
55 | const value = e.target.value;
56 | setInput(value)
57 | if (value.split(' ')[0] === '@' || value === '@') {
58 | setshowPopup(true);
59 | } else {
60 | setshowPopup(false);
61 | }
62 | };
63 |
64 | return (
65 | <>
66 |
67 |
146 |
147 |
148 |
149 |
150 | >
151 | )
152 | }
153 |
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { ThemeProviderProps } from 'next-themes/dist/types'
6 | import { SidebarProvider } from '@/lib/hooks/use-sidebar'
7 | import { TooltipProvider } from '@/components/ui/tooltip'
8 | import { SettingProvider } from '@/lib/hooks/use-setting';
9 |
10 |
11 | export function Providers({ children, ...props }: ThemeProviderProps) {
12 | return (
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/search-related.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { Button } from './ui/button'
5 | import { ArrowRight } from 'lucide-react'
6 | import { useActions, useStreamableValue, useUIState } from 'ai/rsc'
7 | import { AI } from '@/app/action'
8 | import { UserMessage } from './user-message'
9 | import { PartialRelated } from '@/lib/schema/related'
10 | import {useSetting} from '@/lib/hooks/use-setting'
11 |
12 | export interface SearchRelatedProps {
13 | relatedQueries: PartialRelated
14 | }
15 |
16 | export const SearchRelated: React.FC = ({
17 | relatedQueries
18 | }) => {
19 | const { submit } = useActions()
20 | const [, setMessages] = useUIState()
21 | const [data, error, pending] =
22 | useStreamableValue(relatedQueries)
23 | const [keys, setKeys] = useSetting();
24 | const handleSubmit = async (event: React.FormEvent) => {
25 | event.preventDefault()
26 | const formData = new FormData(event.currentTarget as HTMLFormElement)
27 |
28 | // // Get the submitter of the form
29 | const submitter = (event.nativeEvent as SubmitEvent)
30 | .submitter as HTMLInputElement
31 | let query = ''
32 | if (submitter) {
33 | formData.append(submitter.name, submitter.value)
34 | query = submitter.value
35 | }
36 |
37 | const userMessage = {
38 | id: Date.now(),
39 | component:
40 | }
41 | const apikeys = {'llm_api_key':keys.current.llm_api_key,'llm_base_url':keys.current.llm_base_url,'llm_model':keys.current.llm_model,'tavilyserp_api_key':keys.current.tavilyserp_api_key}
42 | const skip=undefined
43 | const responseMessage = await submit(formData,skip,apikeys)
44 | setMessages(currentMessages => [
45 | ...currentMessages,
46 | userMessage,
47 | responseMessage
48 | ])
49 | }
50 |
51 | return (
52 |
70 | )
71 | }
72 |
73 | export default SearchRelated
74 |
--------------------------------------------------------------------------------
/components/search-results-image.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @next/next/no-img-element */
2 | 'use client'
3 |
4 | import { Card, CardContent } from '@/components/ui/card'
5 | import {
6 | Dialog,
7 | DialogContent,
8 | DialogDescription,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger
12 | } from '@/components/ui/dialog'
13 | import {
14 | Carousel,
15 | type CarouselApi,
16 | CarouselContent,
17 | CarouselItem,
18 | CarouselNext,
19 | CarouselPrevious
20 | } from '@/components/ui/carousel'
21 | import { useEffect, useState } from 'react'
22 | import { PlusCircle } from 'lucide-react'
23 |
24 | interface SearchResultsImageSectionProps {
25 | images: string[]
26 | query?: string
27 | }
28 |
29 | export const SearchResultsImageSection: React.FC<
30 | SearchResultsImageSectionProps
31 | > = ({ images, query }) => {
32 | const [api, setApi] = useState()
33 | const [current, setCurrent] = useState(0)
34 | const [count, setCount] = useState(0)
35 | const [selectedIndex, setSelectedIndex] = useState(0)
36 |
37 | // Update the current and count state when the carousel api is available
38 | useEffect(() => {
39 | if (!api) {
40 | return
41 | }
42 |
43 | setCount(api.scrollSnapList().length)
44 | setCurrent(api.selectedScrollSnap() + 1)
45 |
46 | api.on('select', () => {
47 | setCurrent(api.selectedScrollSnap() + 1)
48 | })
49 | }, [api])
50 |
51 | // Scroll to the selected index
52 | useEffect(() => {
53 | if (api) {
54 | api.scrollTo(selectedIndex, true)
55 | }
56 | }, [api, selectedIndex])
57 |
58 | if (!images || images.length === 0) {
59 | return No images found
60 | }
61 |
62 | return (
63 |
64 | {images.slice(0, 4).map((image: any, index: number) => (
65 |
129 | ))}
130 |
131 | )
132 | }
133 |
--------------------------------------------------------------------------------
/components/search-results.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useState } from 'react'
4 | import { AvatarImage, Avatar, AvatarFallback } from '@/components/ui/avatar'
5 | import { CardContent, Card } from '@/components/ui/card'
6 | import { Button } from '@/components/ui/button'
7 | import Link from 'next/link'
8 |
9 | export interface SearchResultsProps {
10 | results: { title: string; url: string; content: string }[]
11 | }
12 |
13 | export function SearchResults({ results }: SearchResultsProps) {
14 | // State to manage whether to display the results
15 | const [showAllResults, setShowAllResults] = useState(false)
16 |
17 | const handleViewMore = () => {
18 | setShowAllResults(true)
19 | }
20 |
21 | const displayedResults = showAllResults ? results : results.slice(0, 3)
22 | const additionalResultsCount = results.length > 3 ? results.length - 3 : 0
23 |
24 | return (
25 |
26 | {displayedResults.map((result: any, index: any) => (
27 |
28 |
29 |
30 |
31 | {result.content}
32 |
33 |
34 |
40 |
41 | {new URL(result.url).hostname[0]}
42 |
43 |
44 |
45 | {new URL(result.url).hostname}
46 |
47 |
48 |
49 |
50 |
51 |
52 | ))}
53 | {!showAllResults && additionalResultsCount > 0 && (
54 |
55 |
56 |
57 |
64 |
65 |
66 |
67 | )}
68 |
69 | )
70 | }
71 |
--------------------------------------------------------------------------------
/components/search-skeleton.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { Skeleton } from './ui/skeleton'
5 |
6 | export const SearchSkeleton = () => {
7 | return (
8 |
9 |
10 |
11 | {Array.from({ length: 4 }).map((_, index) => (
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ))}
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/components/section.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import {
5 | BookCheck,
6 | Image,
7 | MessageCircleMore,
8 | Newspaper,
9 | Repeat2,
10 | Search
11 | } from 'lucide-react'
12 | import React from 'react'
13 | import { Separator } from './ui/separator'
14 |
15 | type SectionProps = {
16 | children: React.ReactNode
17 | className?: string
18 | size?: 'sm' | 'md' | 'lg'
19 | title?: string
20 | separator?: boolean
21 | }
22 |
23 | export const Section: React.FC = ({
24 | children,
25 | className,
26 | size = 'md',
27 | title,
28 | separator = false
29 | }) => {
30 | let icon: React.ReactNode
31 | switch (title) {
32 | case 'Images':
33 | // eslint-disable-next-line jsx-a11y/alt-text
34 | icon =
35 | break
36 | case 'Sources':
37 | icon =
38 | break
39 | case 'Answer':
40 | icon =
41 | break
42 | case 'Related':
43 | icon =
44 | break
45 | case 'Follow-up':
46 | icon =
47 | break
48 | default:
49 | icon =
50 | }
51 |
52 | return (
53 | <>
54 | {separator && }
55 |
61 | {title && (
62 |
63 | {icon}
64 | {title}
65 |
66 | )}
67 | {children}
68 |
69 | >
70 | )
71 | }
72 |
--------------------------------------------------------------------------------
/components/sidebar-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { IconSpinner, IconTrash, IconCopy } from '@/components/ui/icons'
3 |
4 | import { useRouter } from 'next/navigation'
5 | import * as React from 'react'
6 | import { toast } from 'react-hot-toast'
7 |
8 | import {
9 | AlertDialog,
10 | AlertDialogAction,
11 | AlertDialogCancel,
12 | AlertDialogContent,
13 | AlertDialogDescription,
14 | AlertDialogFooter,
15 | AlertDialogHeader,
16 | AlertDialogTitle
17 | } from '@/components/ui/alert-dialog'
18 | import { Button } from '@/components/ui/button'
19 | import {
20 | Tooltip,
21 | TooltipContent,
22 | TooltipTrigger
23 | } from '@/components/ui/tooltip'
24 |
25 | interface SidebarActionsProps {
26 | chatId: string
27 | }
28 |
29 | export function SidebarActions({
30 | chatId,
31 | }: SidebarActionsProps) {
32 | const router = useRouter()
33 | const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
34 |
35 | const [isRemovePending, startRemoveTransition] = React.useTransition()
36 |
37 | return (
38 | <>
39 |
40 |
41 |
42 |
59 |
60 | Share chat
61 |
62 |
63 |
64 |
73 |
74 | Delete chat
75 |
76 |
77 |
78 |
79 |
80 | Are you absolutely sure?
81 |
82 | This will permanently delete your chat message and remove your
83 | data from our servers.
84 |
85 |
86 |
87 |
88 | Cancel
89 |
90 | {
93 | event.preventDefault()
94 | // @ts-ignore
95 | startRemoveTransition(async () => {
96 | localStorage.removeItem(chatId)
97 | setDeleteDialogOpen(false)
98 | router.replace('/')
99 | router.refresh()
100 | toast.success('Chat deleted')
101 | })
102 | }}
103 | >
104 | {isRemovePending && }
105 | Delete
106 |
107 |
108 |
109 |
110 | >
111 | )
112 | }
113 |
--------------------------------------------------------------------------------
/components/sidebar-desktop.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar } from '@/components/sidebar'
2 |
3 | import { ChatHistory } from '@/components/chat-history'
4 |
5 | export async function SidebarDesktop() {
6 |
7 | return (
8 |
9 | {/* @ts-ignore */}
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/components/sidebar-footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | export function SidebarFooter({
4 | children,
5 | className,
6 | ...props
7 | }: React.ComponentProps<'div'>) {
8 | return (
9 |
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | import { motion } from 'framer-motion'
9 |
10 | import { buttonVariants } from '@/components/ui/button'
11 |
12 | import { cn } from '@/lib/utils'
13 | import { Chat } from '@/lib/types'
14 |
15 | interface SidebarItemProps {
16 | index: number
17 | chat: Chat
18 | children: React.ReactNode
19 | }
20 |
21 | export function SidebarItem({ index, chat, children }: SidebarItemProps) {
22 | const pathname = usePathname()
23 |
24 | const isActive = pathname === chat.id
25 |
26 | const shouldAnimate = index === 0 && isActive
27 |
28 | if (!chat) return null
29 |
30 | return (
31 |
50 |
51 |
59 |
60 |
61 | {chat.messages[0].content.slice(0, 50)}
62 |
{chat.id.slice(4)}
63 |
64 |
65 |
66 | {children}
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/components/sidebar-items.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { AnimatePresence, motion } from 'framer-motion'
4 |
5 | import { SidebarActions } from '@/components/sidebar-actions'
6 | import { SidebarItem } from '@/components/sidebar-item'
7 | import { Chat } from '@/lib/types'
8 |
9 | interface SidebarItemsProps {
10 | chats?: Chat[]
11 | }
12 |
13 | export function SidebarItems({ chats }: SidebarItemsProps) {
14 | if (!chats?.length) return null
15 |
16 | return (
17 |
18 | {chats.map(
19 | (chat, index) =>
20 | chat && (
21 |
28 |
29 |
32 |
33 |
34 | )
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ClearHistory } from '@/components/clear-history'
3 | import { SidebarItems } from '@/components/sidebar-items'
4 | import { Chat } from '@/lib/types'
5 | import { IconRefresh } from './ui/icons'
6 | import { Button } from './ui/button'
7 | import {
8 | Tooltip,
9 | TooltipContent,
10 | TooltipTrigger
11 | } from '@/components/ui/tooltip'
12 | interface SidebarListProps {
13 | userId?: string
14 | children?: React.ReactNode
15 | }
16 |
17 |
18 | export function SidebarList({ userId }: SidebarListProps) {
19 |
20 | const chatKeys: string[] = Object.keys(localStorage).filter(key => key.startsWith('cid_'));
21 | chatKeys.sort().reverse();
22 |
23 | const chats:Chat[] = chatKeys.map(key => {
24 | const messages = JSON.parse(localStorage.getItem(key) || "");
25 | return {
26 | id: key,
27 | messages
28 | };
29 | });
30 |
31 | return (
32 |
33 |
34 | {chats?.length ? (
35 |
36 |
37 |
38 | ) : (
39 |
40 |
No chat history
41 |
42 | )}
43 |
44 |
45 |
46 |
47 | {/* */}
48 |
49 | Force Reload Page
50 |
51 | 0} />
52 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/components/sidebar-mobile.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
4 |
5 | import { Sidebar } from '@/components/sidebar'
6 | import { Button } from '@/components/ui/button'
7 |
8 | import { IconSidebar } from '@/components/ui/icons'
9 |
10 | interface SidebarMobileProps {
11 | children: React.ReactNode
12 | }
13 |
14 | export function SidebarMobile({ children }: SidebarMobileProps) {
15 | return (
16 |
17 |
18 |
22 |
23 |
24 | {children}
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/components/sidebar-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { useSidebar } from '@/lib/hooks/use-sidebar'
6 | import { Button } from '@/components/ui/button'
7 | import { IconSidebar } from '@/components/ui/icons'
8 |
9 | export function SidebarToggle() {
10 | const { toggleSidebar } = useSidebar()
11 |
12 | return (
13 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { useSidebar } from '@/lib/hooks/use-sidebar'
6 | import { cn } from '@/lib/utils'
7 |
8 | export interface SidebarProps extends React.ComponentProps<'div'> {}
9 |
10 | export function Sidebar({ className, children }: SidebarProps) {
11 | const { isSidebarOpen, isLoading } = useSidebar()
12 |
13 | return (
14 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === 'production') return null
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
5 | import { useTheme } from "next-themes"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | DropdownMenu,
10 | DropdownMenuContent,
11 | DropdownMenuItem,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu"
14 |
15 | export function ThemeToggle() {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
26 |
27 |
28 | setTheme("light")}>
29 | Light
30 |
31 | setTheme("dark")}>
32 | Dark
33 |
34 | setTheme("system")}>
35 | System
36 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/components/tool-badge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Search } from 'lucide-react'
3 | import { Badge } from './ui/badge'
4 |
5 | type ToolBadgeProps = {
6 | tool: string
7 | children: React.ReactNode
8 | className?: string
9 | }
10 |
11 | export const ToolBadge: React.FC = ({
12 | tool,
13 | children,
14 | className
15 | }) => {
16 | const icon: Record = {
17 | search:
18 | }
19 |
20 | return (
21 |
22 | {icon[tool]}
23 | {children}
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { buttonVariants } from '@/components/ui/button'
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = 'AlertDialogHeader'
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = 'AlertDialogFooter'
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground'
18 | }
19 | },
20 | defaultVariants: {
21 | variant: 'default'
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
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 rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 shadow-none hover:underline'
22 | },
23 | size: {
24 | default: 'h-8 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'size-8 p-0'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/codeblock.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3 |
4 | 'use client'
5 |
6 | import { FC, memo } from 'react'
7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9 |
10 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12 | import { Button } from '@/components/ui/button'
13 |
14 | interface Props {
15 | language: string
16 | value: string
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: '.js',
25 | python: '.py',
26 | java: '.java',
27 | c: '.c',
28 | cpp: '.cpp',
29 | 'c++': '.cpp',
30 | 'c#': '.cs',
31 | ruby: '.rb',
32 | php: '.php',
33 | swift: '.swift',
34 | 'objective-c': '.m',
35 | kotlin: '.kt',
36 | typescript: '.ts',
37 | go: '.go',
38 | perl: '.pl',
39 | rust: '.rs',
40 | scala: '.scala',
41 | haskell: '.hs',
42 | lua: '.lua',
43 | shell: '.sh',
44 | sql: '.sql',
45 | html: '.html',
46 | css: '.css'
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | }
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = ''
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length))
55 | }
56 | return lowercase ? result.toLowerCase() : result
57 | }
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === 'undefined') {
64 | return
65 | }
66 | const fileExtension = programmingLanguages[language] || '.file'
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`
71 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return
76 | }
77 |
78 | const blob = new Blob([value], { type: 'text/plain' })
79 | const url = URL.createObjectURL(blob)
80 | const link = document.createElement('a')
81 | link.download = fileName
82 | link.href = url
83 | link.style.display = 'none'
84 | document.body.appendChild(link)
85 | link.click()
86 | document.body.removeChild(link)
87 | URL.revokeObjectURL(url)
88 | }
89 |
90 | const onCopy = () => {
91 | if (isCopied) return
92 | copyToClipboard(value)
93 | }
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
109 |
118 |
119 |
120 |
141 | {value}
142 |
143 |
144 | )
145 | })
146 | CodeBlock.displayName = 'CodeBlock'
147 |
148 | export { CodeBlock }
149 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = 'DialogHeader'
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = 'DialogFooter'
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from 'react'
2 | import ReactMarkdown, { Options } from 'react-markdown'
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) =>
7 | prevProps.children === nextProps.children &&
8 | prevProps.className === nextProps.className
9 | )
10 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SelectPrimitive from '@radix-ui/react-select'
5 |
6 | import { cn } from '@/lib/utils'
7 | import {
8 | IconArrowDown,
9 | IconCheck,
10 | IconChevronUpDown
11 | } from '@/components/ui/icons'
12 |
13 | const Select = SelectPrimitive.Root
14 |
15 | const SelectGroup = SelectPrimitive.Group
16 |
17 | const SelectValue = SelectPrimitive.Value
18 |
19 | const SelectTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | {children}
32 |
33 |
34 |
35 |
36 | ))
37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38 |
39 | const SelectContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, position = 'popper', ...props }, ref) => (
43 |
44 |
54 |
61 | {children}
62 |
63 |
64 |
65 | ))
66 | SelectContent.displayName = SelectPrimitive.Content.displayName
67 |
68 | const SelectLabel = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | SelectLabel.displayName = SelectPrimitive.Label.displayName
79 |
80 | const SelectItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, children, ...props }, ref) => (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113 |
114 | export {
115 | Select,
116 | SelectGroup,
117 | SelectValue,
118 | SelectTrigger,
119 | SelectContent,
120 | SelectLabel,
121 | SelectItem,
122 | SelectSeparator
123 | }
124 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SheetPrimitive from '@radix-ui/react-dialog'
5 | import { cva, type VariantProps } from 'class-variance-authority'
6 | import { IconClose } from '@/components/ui/icons'
7 |
8 | import { cn } from '@/lib/utils'
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | 'fixed z-50 gap-4 bg-background p-6 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',
35 | {
36 | variants: {
37 | side: {
38 | top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
39 | bottom:
40 | 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
41 | 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',
42 | right:
43 | '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'
44 | }
45 | },
46 | defaultVariants: {
47 | side: 'right'
48 | }
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = 'left', className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = 'SheetHeader'
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = 'SheetFooter'
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SwitchPrimitives from '@radix-ui/react-switch'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/user-message.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import React from 'react'
3 |
4 | type UserMessageProps = {
5 | message: string
6 | isFirstMessage?: boolean
7 | }
8 |
9 | export const UserMessage: React.FC = ({
10 | message,
11 | isFirstMessage
12 | }) => {
13 | return (
14 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/lib/agents/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './task-manager'
2 | export * from './inquire'
3 | export * from './query-suggestor'
4 | export * from './researcher'
5 |
--------------------------------------------------------------------------------
/lib/agents/inquire.tsx:
--------------------------------------------------------------------------------
1 | import { OpenAI } from '@ai-sdk/openai'
2 | import { Copilot } from '@/components/copilot'
3 | import { createStreamableUI, createStreamableValue } from 'ai/rsc'
4 | import { ExperimentalMessage, experimental_streamObject } from 'ai'
5 | import { PartialInquiry, inquirySchema } from '@/lib/schema/inquiry'
6 |
7 | export async function inquire(
8 | uiStream: ReturnType,
9 | messages: ExperimentalMessage[],
10 | llm_base_url:string,
11 | llm_api_key:string,
12 | llm_model:string
13 | ) {
14 | const openai = new OpenAI({
15 | baseUrl: llm_base_url, // optional base URL for proxies etc.
16 | apiKey: llm_api_key, // optional API key, default to env property OPENAI_API_KEY
17 | organization: '' // optional organization
18 | })
19 | const objectStream = createStreamableValue()
20 | uiStream.update()
21 | console.log('inquire start', uiStream,messages,llm_base_url,llm_api_key,llm_model)
22 | let finalInquiry: PartialInquiry = {}
23 | await experimental_streamObject({
24 | model: openai.chat(llm_model || 'gpt-3.5-turbo-0125'),
25 | system: `As a professional web researcher, your role is to deepen your understanding of the user's input by conducting further inquiries when necessary.
26 | After receiving an initial response from the user, carefully assess whether additional questions are absolutely essential to provide a comprehensive and accurate answer. Only proceed with further inquiries if the available information is insufficient or ambiguous.
27 |
28 | When crafting your inquiry, structure it as follows:
29 | {
30 | "question": "A clear, concise question that seeks to clarify the user's intent or gather more specific details.",
31 | "options": [
32 | {"value": "option1", "label": "A predefined option that the user can select"},
33 | {"value": "option2", "label": "Another predefined option"},
34 | ...
35 | ],
36 | "allowsInput": true/false, // Indicates whether the user can provide a free-form input
37 | "inputLabel": "A label for the free-form input field, if allowed",
38 | "inputPlaceholder": "A placeholder text to guide the user's free-form input"
39 | }
40 |
41 | For example:
42 | {
43 | "question": "What specific information are you seeking about Rivian?",
44 | "options": [
45 | {"value": "history", "label": "History"},
46 | {"value": "products", "label": "Products"},
47 | {"value": "investors", "label": "Investors"},
48 | {"value": "partnerships", "label": "Partnerships"},
49 | {"value": "competitors", "label": "Competitors"}
50 | ],
51 | "allowsInput": true,
52 | "inputLabel": "If other, please specify",
53 | "inputPlaceholder": "e.g., Specifications"
54 | }
55 |
56 | By providing predefined options, you guide the user towards the most relevant aspects of their query, while the free-form input allows them to provide additional context or specific details not covered by the options.
57 | Remember, your goal is to gather the necessary information to deliver a thorough and accurate response.
58 | Please match the language of the response to the user's language.
59 | `,
60 | messages,
61 | schema: inquirySchema
62 | })
63 | .then(async result => {
64 | console.log("inquire",result)
65 | for await (const obj of result.partialObjectStream) {
66 | if (obj) {
67 | objectStream.update(obj)
68 | finalInquiry = obj
69 | }
70 | }
71 | })
72 | .finally(() => {
73 | objectStream.done()
74 | })
75 |
76 | return finalInquiry
77 | }
78 |
--------------------------------------------------------------------------------
/lib/agents/query-suggestor.tsx:
--------------------------------------------------------------------------------
1 | import { createStreamableUI, createStreamableValue } from 'ai/rsc'
2 | import { ExperimentalMessage, experimental_streamObject } from 'ai'
3 | import { PartialRelated, relatedSchema } from '@/lib/schema/related'
4 | import { Section } from '@/components/section'
5 | import SearchRelated from '@/components/search-related'
6 | import { OpenAI } from '@ai-sdk/openai'
7 |
8 | export async function querySuggestor(
9 | uiStream: ReturnType,
10 | messages: ExperimentalMessage[],
11 | llm_base_url:string,
12 | llm_api_key:string,
13 | llm_model:string
14 | ) {
15 | const openai = new OpenAI({
16 | baseUrl: llm_base_url, // optional base URL for proxies etc.
17 | apiKey: llm_api_key, // optional API key, default to env property OPENAI_API_KEY
18 | organization: '' // optional organization
19 | })
20 | const objectStream = createStreamableValue()
21 | uiStream.append(
22 |
25 | )
26 |
27 | await experimental_streamObject({
28 | model: openai.chat(llm_model || 'gpt-3.5-turbo-0125'),
29 | system: `As a professional web researcher, your task is to generate a set of three queries that explore the subject matter more deeply, building upon the initial query and the information uncovered in its search results.
30 |
31 | For instance, if the original query was "Starship's third test flight key milestones", your output should follow this format:
32 |
33 | "{
34 | "related": [
35 | "What were the primary objectives achieved during Starship's third test flight?",
36 | "What factors contributed to the ultimate outcome of Starship's third test flight?",
37 | "How will the results of the third test flight influence SpaceX's future development plans for Starship?"
38 | ]
39 | }"
40 |
41 | Aim to create queries that progressively delve into more specific aspects, implications, or adjacent topics related to the initial query. The goal is to anticipate the user's potential information needs and guide them towards a more comprehensive understanding of the subject matter.
42 | Please match the language of the response to the user's language.`,
43 | messages,
44 | schema: relatedSchema
45 | })
46 | .then(async result => {
47 | for await (const obj of result.partialObjectStream) {
48 | objectStream.update(obj)
49 | }
50 | })
51 | .finally(() => {
52 | objectStream.done()
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/lib/agents/task-manager.tsx:
--------------------------------------------------------------------------------
1 | import { ExperimentalMessage, experimental_generateObject } from 'ai'
2 | import { OpenAI } from '@ai-sdk/openai'
3 | import { nextActionSchema } from '../schema/next-action'
4 |
5 | // Decide whether inquiry is required for the user input
6 | export async function taskManager(messages: ExperimentalMessage[],
7 | llm_base_url:string,
8 | llm_api_key:string,
9 | llm_model:string
10 | ) {
11 | const openai = new OpenAI({
12 | baseUrl: llm_base_url, // optional base URL for proxies etc.
13 | apiKey: llm_api_key, // optional API key, default to env property OPENAI_API_KEY
14 | organization: '' // optional organization
15 | })
16 |
17 | try {
18 | const result = await experimental_generateObject({
19 | model: openai.chat(llm_model || 'gpt-3.5-turbo-0125'),
20 | system: `As a professional web researcher, your primary objective is to fully comprehend the user's query, conduct thorough web searches to gather the necessary information, and provide an appropriate response.
21 | To achieve this, you must first analyze the user's input and determine the optimal course of action. You have two options at your disposal:
22 | 1. "proceed": If the provided information is sufficient to address the query effectively, choose this option to proceed with the research and formulate a response.
23 | 2. "inquire": If you believe that additional information from the user would enhance your ability to provide a comprehensive response, select this option. You may present a form to the user, offering default selections or free-form input fields, to gather the required details.
24 | Your decision should be based on a careful assessment of the context and the potential for further information to improve the quality and relevance of your response.
25 | For example, if the user asks, "What are the key features of the latest iPhone model?", you may choose to "proceed" as the query is clear and can be answered effectively with web research alone.
26 | However, if the user asks, "What's the best smartphone for my needs?", you may opt to "inquire" and present a form asking about their specific requirements, budget, and preferred features to provide a more tailored recommendation.
27 | Make your choice wisely to ensure that you fulfill your mission as a web researcher effectively and deliver the most valuable assistance to the user.
28 | `,
29 | messages,
30 | schema: nextActionSchema
31 | })
32 |
33 | return result
34 | } catch (error) {
35 | console.error(error)
36 | return null
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset
11 | )
12 | }
13 |
14 | window.addEventListener('scroll', handleScroll, { passive: true })
15 | handleScroll()
16 |
17 | return () => {
18 | window.removeEventListener('scroll', handleScroll)
19 | }
20 | }, [offset])
21 |
22 | return isAtBottom
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue)
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key)
12 | if (item) {
13 | setStoredValue(JSON.parse(item))
14 | }
15 | }, [key])
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value)
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value))
22 | }
23 | return [storedValue, setValue]
24 | }
25 |
--------------------------------------------------------------------------------
/lib/hooks/use-setting.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { createContext, useContext, useState, useEffect } from 'react';
3 | import {KeyScheme} from '@/lib/types'
4 | import { ReactNode, Dispatch } from 'react';
5 | import { useLocalStorage } from '@/lib/hooks/use-local-storage';
6 |
7 | export const initialPreviewToken = {
8 | scheme: "",
9 | llm_api_key: "",
10 | llm_model: "",
11 | llm_base_url: "",
12 | tavilyserp_api_key: "",
13 | google_api_key: "",
14 | google_cse_id: "",
15 | bing_api_key: "",
16 | };
17 |
18 | export const initialKeyScheme: KeyScheme = {
19 | current: { ...initialPreviewToken },
20 | keys1: { ...initialPreviewToken },
21 | keys2: { ...initialPreviewToken },
22 | keys3: { ...initialPreviewToken },
23 | };
24 |
25 | type SettingContextValue = [KeyScheme, Dispatch>];
26 | const SettingContext = createContext([initialKeyScheme, () => {}]);
27 |
28 | export function SettingProvider({ children }: { children: ReactNode }) {
29 | const [Keys, setKeys] = useLocalStorage('ai-token', initialKeyScheme) as SettingContextValue;
30 |
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | }
37 |
38 |
39 | export function useSetting() {
40 | return useContext(SettingContext);
41 | }
--------------------------------------------------------------------------------
/lib/hooks/use-sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | const LOCAL_STORAGE_KEY = 'sidebar'
6 |
7 | interface SidebarContext {
8 | isSidebarOpen: boolean
9 | toggleSidebar: () => void
10 | isLoading: boolean
11 | }
12 |
13 | const SidebarContext = React.createContext(
14 | undefined
15 | )
16 |
17 | export function useSidebar() {
18 | const context = React.useContext(SidebarContext)
19 | if (!context) {
20 | throw new Error('useSidebarContext must be used within a SidebarProvider')
21 | }
22 | return context
23 | }
24 |
25 | interface SidebarProviderProps {
26 | children: React.ReactNode
27 | }
28 |
29 | export function SidebarProvider({ children }: SidebarProviderProps) {
30 | const [isSidebarOpen, setSidebarOpen] = React.useState(false)
31 | const [isLoading, setLoading] = React.useState(true)
32 |
33 | React.useEffect(() => {
34 | const value = localStorage.getItem(LOCAL_STORAGE_KEY)
35 | if (value) {
36 | setSidebarOpen(JSON.parse(value))
37 | }
38 | setLoading(false)
39 | }, [])
40 |
41 | const toggleSidebar = () => {
42 | setSidebarOpen(value => {
43 | const newState = !value
44 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(newState))
45 | return newState
46 | })
47 | }
48 |
49 | if (isLoading) {
50 | return null
51 | }
52 |
53 | return (
54 |
57 | {children}
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/lib/schema/inquiry.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const inquirySchema = z.object({
5 | question: z.string().describe('The inquiry question'),
6 | options: z
7 | .array(
8 | z.object({
9 | value: z.string(),
10 | label: z.string()
11 | })
12 | )
13 | .describe('The inquiry options'),
14 | allowsInput: z.boolean().describe('Whether the inquiry allows for input'),
15 | inputLabel: z.string().optional().describe('The label for the input field'),
16 | inputPlaceholder: z
17 | .string()
18 | .optional()
19 | .describe('The placeholder for the input field')
20 | })
21 |
22 | export type PartialInquiry = DeepPartial
23 |
--------------------------------------------------------------------------------
/lib/schema/next-action.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const nextActionSchema = z.object({
5 | next: z.enum(['inquire', 'proceed']) // "generate_ui"
6 | })
7 |
8 | export type NextAction = DeepPartial
9 |
--------------------------------------------------------------------------------
/lib/schema/related.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const relatedSchema = z.object({
5 | items: z
6 | .array(
7 | z.object({
8 | query: z.string()
9 | })
10 | )
11 | .length(3)
12 | })
13 | export type PartialRelated = DeepPartial
14 |
--------------------------------------------------------------------------------
/lib/schema/search.tsx:
--------------------------------------------------------------------------------
1 | import { DeepPartial } from 'ai'
2 | import { z } from 'zod'
3 |
4 | export const searchSchema = z.object({
5 | query: z.string().describe('The query to search for'),
6 | max_results: z
7 | .number()
8 | .max(20)
9 | .default(5)
10 | .describe('The maximum number of results to return'),
11 | search_depth: z
12 | .enum(['basic', 'advanced'])
13 | .default('basic')
14 | .describe('The depth of the search')
15 | })
16 |
17 | export type PartialInquiry = DeepPartial
18 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | export interface Chat extends Record {
4 | id: string
5 | title?: string
6 | createdAt?: Date
7 | userId?: string
8 | path?: string
9 | messages: Message[]
10 | sharePath?: string
11 | }
12 |
13 | export type ServerActionResult = Promise<
14 | | Result
15 | | {
16 | error: string
17 | }
18 | >
19 |
20 | export type PreviewToken = {
21 | scheme: string;
22 | llm_api_key: string;
23 | llm_model: string;
24 | llm_base_url: string;
25 | tavilyserp_api_key: string;
26 | google_api_key: string;
27 | google_cse_id: string;
28 | bing_api_key: string;
29 | };
30 |
31 | export interface KeyScheme {
32 | [key: string]: PreviewToken;
33 | }
34 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | import { customAlphabet } from 'nanoid'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export const nanoid = customAlphabet(
10 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11 | 7
12 | ) // 7-character random string
13 |
14 | export function getRandomColor(): string {
15 | const letters = '0123456789ABCDEF';
16 | let color = '#';
17 | for (let i = 0; i < 6; i++) {
18 | color += letters[Math.floor(Math.random() * 16)];
19 | }
20 | return color;
21 | }
22 |
23 | export function getRandomGradient(dark: boolean): string {
24 | const randomHue = (): number => Math.floor(Math.random() * 360);
25 | const randomSaturation = (): number => Math.floor(Math.random() * 100);
26 | const fixedLightness = dark ? 60 : 96;
27 |
28 | const color1 = `hsl(${randomHue()}, ${randomSaturation()}%, ${fixedLightness}%)`;
29 | const color2 = `hsl(${randomHue()}, ${randomSaturation()}%, ${fixedLightness}%)`;
30 | const color3 = `hsl(${randomHue()}, ${randomSaturation()}%, ${fixedLightness}%)`;
31 |
32 | const angle = Math.floor(Math.random() * 360);
33 |
34 | const gradientType = Math.random() < 0.5 ? 'linear' : 'radial';
35 |
36 | if (gradientType === 'linear') {
37 | return `linear-gradient(${angle}deg, ${color1}, ${color2}, ${color3})`;
38 | } else {
39 | const centerX = Math.floor(Math.random() * 100);
40 | const centerY = Math.floor(Math.random() * 100);
41 | return `radial-gradient(circle at ${centerX}% ${centerY}%, ${color1}, ${color2}, ${color3})`;
42 | }
43 | }
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | // //for vercel
2 | // /** @type {import('next').NextConfig} */
3 | // const nextConfig = {
4 | // env: {
5 | // NEXT_PUBLIC_API_URL: ''
6 | // },
7 | // images: {
8 | // domains: [
9 | // 'lh3.googleusercontent.com',
10 | // 'oaidalleapiprodscus.blob.core.windows.net',
11 | // ],
12 | // }
13 | // }
14 | // export default nextConfig;
15 |
16 | //for tauri use this part:
17 | /** @type {import('next').NextConfig} */
18 | const nextConfig = {
19 | env: {
20 | NEXT_PUBLIC_API_URL: 'http://localhost:6677'
21 | },
22 | ...(process.env.NODE_ENV === 'production' && {
23 | output: 'export',
24 | }),
25 | }
26 | export default nextConfig;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Next-Langchain-Tauri",
3 | "version": "0.3.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 3100",
7 | "build": "next build",
8 | "start": "next start -p 3100",
9 | "lint": "next lint",
10 | "build-server": "esbuild ./server/server.ts --bundle --platform=node --outfile=./build/server.js",
11 | "pkg-server": "pnpm build-server && pkg build/server.js -o ./src-tauri/bin/server-x86_64-apple-darwin"
12 | },
13 | "dependencies": {
14 | "@ai-sdk/openai": "^0.0.2",
15 | "@fastify/cors": "^9.0.1",
16 | "@langchain/community": "^0.0.44",
17 | "@langchain/core": "^0.1.54",
18 | "@langchain/openai": "^0.0.23",
19 | "@radix-ui/react-alert-dialog": "^1.0.5",
20 | "@radix-ui/react-avatar": "^1.0.4",
21 | "@radix-ui/react-checkbox": "^1.0.4",
22 | "@radix-ui/react-collapsible": "^1.0.3",
23 | "@radix-ui/react-dialog": "^1.0.5",
24 | "@radix-ui/react-dropdown-menu": "^2.0.6",
25 | "@radix-ui/react-icons": "^1.3.0",
26 | "@radix-ui/react-label": "^2.0.2",
27 | "@radix-ui/react-popover": "^1.0.7",
28 | "@radix-ui/react-select": "^2.0.0",
29 | "@radix-ui/react-separator": "^1.0.3",
30 | "@radix-ui/react-slot": "^1.0.2",
31 | "@radix-ui/react-switch": "^1.0.3",
32 | "@radix-ui/react-tabs": "^1.0.4",
33 | "@radix-ui/react-tooltip": "^1.0.7",
34 | "@supabase/supabase-js": "^2.43.1",
35 | "@tailwindcss/typography": "^0.5.12",
36 | "@tauri-apps/api": "^1.5.3",
37 | "ai": "^3.0.19",
38 | "axios": "^1.6.8",
39 | "cheerio": "1.0.0-rc.12",
40 | "class-variance-authority": "^0.7.0",
41 | "clsx": "^2.1.0",
42 | "cmdk": "^0.2.1",
43 | "crypto-browserify": "^3.12.0",
44 | "downloadjs": "^1.4.7",
45 | "duck-duck-scrape": "2.2.5",
46 | "embla-carousel-react": "^8.0.2",
47 | "esbuild": "^0.20.2",
48 | "exa-js": "^1.0.12",
49 | "fastify": "^4.26.2",
50 | "framer-motion": "^11.0.25",
51 | "geist": "^1.3.0",
52 | "html-to-image": "^1.11.11",
53 | "langchain": "^0.1.31",
54 | "lucide-react": "^0.363.0",
55 | "nanoid": "^5.0.6",
56 | "next": "14.1.4",
57 | "next-themes": "^0.3.0",
58 | "openai": "^4.33.0",
59 | "react": "^18.2.0",
60 | "react-dom": "^18.2.0",
61 | "react-hot-toast": "^2.4.1",
62 | "react-intersection-observer": "^9.8.1",
63 | "react-markdown": "^9.0.1",
64 | "react-syntax-highlighter": "^15.5.0",
65 | "react-textarea-autosize": "^8.5.3",
66 | "rehype-external-links": "^3.0.0",
67 | "remark-gfm": "^4.0.0",
68 | "remark-math": "^6.0.0",
69 | "tailwind-merge": "^2.2.2",
70 | "tailwindcss-animate": "^1.0.7",
71 | "zod": "^3.22.5"
72 | },
73 | "devDependencies": {
74 | "@tauri-apps/cli": "^1.5.11",
75 | "@types/d3-scale": "^4.0.8",
76 | "@types/downloadjs": "^1.4.6",
77 | "@types/node": "^20.12.5",
78 | "@types/react": "^18.2.74",
79 | "@types/react-dom": "^18.2.24",
80 | "@types/react-syntax-highlighter": "^15.5.11",
81 | "autoprefixer": "^10.4.19",
82 | "eslint": "8.57.0",
83 | "eslint-config-next": "14.1.4",
84 | "postcss": "^8.4.38",
85 | "prettier": "^3.2.5",
86 | "tailwindcss": "^3.4.3",
87 | "typescript": "^5.4.4"
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: 'lf',
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: true,
7 | arrowParens: 'avoid',
8 | tabWidth: 2,
9 | trailingComma: 'none',
10 | importOrder: [
11 | '^(react/(.*)$)|^(react$)',
12 | '^(next/(.*)$)|^(next$)',
13 | '',
14 | '',
15 | '^types$',
16 | '^@/types/(.*)$',
17 | '^@/config/(.*)$',
18 | '^@/lib/(.*)$',
19 | '^@/hooks/(.*)$',
20 | '^@/components/ui/(.*)$',
21 | '^@/components/(.*)$',
22 | '^@/registry/(.*)$',
23 | '^@/styles/(.*)$',
24 | '^@/app/(.*)$',
25 | '',
26 | '^[./]'
27 | ],
28 | importOrderSeparation: false,
29 | importOrderSortSpecifiers: true,
30 | importOrderBuiltinModulesToTop: true,
31 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
32 | importOrderMergeDuplicateImports: true,
33 | importOrderCombineTypeAndValueImports: true
34 | }
35 |
--------------------------------------------------------------------------------
/server/agent.ts:
--------------------------------------------------------------------------------
1 | import { StreamingTextResponse } from "ai";
2 | import { HttpResponseOutputParser } from "langchain/output_parsers";
3 | import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
4 | import { ChatOpenAI,OpenAIEmbeddings } from "@langchain/openai";
5 | import { DallEAPIWrapper } from "./custom/tools/dalle/dalle";
6 | import { DuckDuckGoSearch } from "@langchain/community/tools/duckduckgo_search";
7 | import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
8 | import { BingSerpAPI } from "@langchain/community/tools/bingserpapi";
9 | import { GoogleCustomSearch } from "@langchain/community/tools/google_custom_search";
10 | import { AIMessage, HumanMessage } from "@langchain/core/messages";
11 | import { WebBrowser } from "langchain/tools/webbrowser";
12 | import {
13 | ChatPromptTemplate,
14 | MessagesPlaceholder,
15 | } from "@langchain/core/prompts";
16 |
17 | const convertMessageToLangChainMessage = (message: any) => {
18 | if (message.role === "user") {
19 | return new HumanMessage(message.content);
20 | } else if (message.role === "assistant") {
21 | return new AIMessage(message.content);
22 | }
23 | };
24 |
25 |
26 | export async function Chat(body: any) {
27 | // console.log(body);
28 | process.env.TAVILY_API_KEY = body.previewToken.tavilyserp_api_key;
29 | process.env.GOOGLE_API_KEY = body.previewToken.google_api_key;
30 | process.env.GOOGLE_CSE_ID = body.previewToken.google_cse_id;
31 | const llmBaseURL = body.previewToken?.llm_base_url || 'https://api.openai.com/v1'
32 | const messages = (body.messages ?? []).filter(
33 | (message: any) =>
34 | message.role === "user" || message.role === "assistant",
35 | ).map(convertMessageToLangChainMessage);
36 | const model = new ChatOpenAI({
37 | temperature: 0.7,
38 | modelName: (body.annotations?body.annotations[0]:undefined) || body.previewToken.llm_model || 'gpt-3.5-turbo-0125',
39 | openAIApiKey: body.previewToken.llm_api_key,
40 | configuration: { baseURL:llmBaseURL },
41 | maxTokens: 2048,
42 | streaming: true
43 | });
44 | if(!body.messages.slice(-1)[0].function_call){
45 | const outputParser = new HttpResponseOutputParser()
46 | const stream = await model.pipe(outputParser).stream(messages);
47 | return new StreamingTextResponse(stream);
48 | }
49 | // console.log(body)
50 | const previousMessages = messages
51 | .slice(0, -1)
52 | const currentMessageContent = messages[messages.length - 1].content;
53 | // console.log(previousMessages,currentMessageContent)
54 |
55 | var tools: any[] = [];
56 | tools.push(new DuckDuckGoSearch({ maxResults: 5 }));
57 | if (body.previewToken.tavilyserp_api_key) {
58 | tools.push(new TavilySearchResults({maxResults: 5}));
59 | }
60 | if (body.previewToken.bing_api_key) {
61 | tools.push(new BingSerpAPI(body.previewToken.bing_api_key));
62 | }
63 | if (body.previewToken.google_api_key) {
64 | tools.push(new GoogleCustomSearch());
65 | }
66 | const embeddings = new OpenAIEmbeddings( {openAIApiKey:body.previewToken.llm_api_key,configuration: { baseURL:llmBaseURL }});
67 | const browser = new WebBrowser({ model, embeddings });
68 | tools.push(browser);
69 | tools.push(new DallEAPIWrapper({
70 | n: 1,
71 | modelName: "dall-e-3",
72 | openAIApiKey: body.previewToken.llm_api_key,
73 | baseURL:llmBaseURL
74 | }))
75 | const AGENT_SYSTEM_PROMPT = "You are a helpful assistant can play any role and reply as the role user calls by '@' symbol . Here's one of the roles:"
76 | const prompt = ChatPromptTemplate.fromMessages([
77 | ["system", AGENT_SYSTEM_PROMPT],
78 | new MessagesPlaceholder("chat_history"),
79 | ["human", "{input}"],
80 | new MessagesPlaceholder("agent_scratchpad"),
81 | ]);
82 |
83 | const agent = await createOpenAIFunctionsAgent({
84 | llm:model,
85 | tools,
86 | prompt,
87 | });
88 |
89 | const agentExecutor = new AgentExecutor({
90 | agent,
91 | tools,
92 | });
93 |
94 |
95 | const logStream = await agentExecutor.streamLog({
96 | input: currentMessageContent,
97 | chat_history: previousMessages,
98 | });
99 |
100 | const encoder = new TextEncoder()
101 |
102 | const transformStream = new ReadableStream({
103 | async start(controller) {
104 | for await (const chunk of logStream) {
105 | if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") {
106 | const addOp = chunk.ops[0];
107 | // console.log(addOp.path,addOp.value)
108 | if (addOp.path.startsWith("/logs/ChatOpenAI") && addOp.path.includes("stream") &&
109 | typeof addOp.value === "string" &&
110 | addOp.value.length
111 | ) {
112 | controller.enqueue(encoder.encode(addOp.value));
113 | }
114 | if(addOp.path.startsWith('/logs/BingSerpAPI/final_output') || addOp.path.startsWith('/logs/GoogleCustomSearch/final_output') || addOp.path.startsWith('/logs/TavilySearchResults/final_output')){
115 | controller.enqueue(encoder.encode('\n\n---\n\n'+ addOp.value.output.split('\n\n').map((line:string)=>line.split(']')[1]).join('\n\n') +'---\n\n'));
116 | }
117 | // if(addOp.path.startsWith('/logs/DallEAPIWrapper/final_output'))controller.enqueue(encoder.encode(``));
118 | }
119 | }
120 | controller.close();
121 | },
122 | });
123 |
124 | return new StreamingTextResponse(transformStream);
125 | }
--------------------------------------------------------------------------------
/server/custom/llm/gemini/chat_models.d.ts:
--------------------------------------------------------------------------------
1 | import type { SafetySetting } from "@fuyun/generative-ai";
2 | import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager";
3 | import { BaseMessage } from "@langchain/core/messages";
4 | import { ChatGenerationChunk, ChatResult } from "@langchain/core/outputs";
5 | import { BaseChatModel, type BaseChatModelParams } from "@langchain/core/language_models/chat_models";
6 | export type BaseMessageExamplePair = {
7 | input: BaseMessage;
8 | output: BaseMessage;
9 | };
10 | /**
11 | * An interface defining the input to the ChatGoogleGenerativeAI class.
12 | */
13 | export interface GoogleGenerativeAIChatInput extends BaseChatModelParams {
14 | /**
15 | * Model Name to use
16 | *
17 | * Note: The format must follow the pattern - `{model}`
18 | */
19 | modelName?: string;
20 | /**
21 | * Controls the randomness of the output.
22 | *
23 | * Values can range from [0.0,1.0], inclusive. A value closer to 1.0
24 | * will produce responses that are more varied and creative, while
25 | * a value closer to 0.0 will typically result in less surprising
26 | * responses from the model.
27 | *
28 | * Note: The default value varies by model
29 | */
30 | temperature?: number;
31 | /**
32 | * Maximum number of tokens to generate in the completion.
33 | */
34 | maxOutputTokens?: number;
35 | /**
36 | * Top-p changes how the model selects tokens for output.
37 | *
38 | * Tokens are selected from most probable to least until the sum
39 | * of their probabilities equals the top-p value.
40 | *
41 | * For example, if tokens A, B, and C have a probability of
42 | * .3, .2, and .1 and the top-p value is .5, then the model will
43 | * select either A or B as the next token (using temperature).
44 | *
45 | * Note: The default value varies by model
46 | */
47 | topP?: number;
48 | /**
49 | * Top-k changes how the model selects tokens for output.
50 | *
51 | * A top-k of 1 means the selected token is the most probable among
52 | * all tokens in the model’s vocabulary (also called greedy decoding),
53 | * while a top-k of 3 means that the next token is selected from
54 | * among the 3 most probable tokens (using temperature).
55 | *
56 | * Note: The default value varies by model
57 | */
58 | topK?: number;
59 | /**
60 | * The set of character sequences (up to 5) that will stop output generation.
61 | * If specified, the API will stop at the first appearance of a stop
62 | * sequence.
63 | *
64 | * Note: The stop sequence will not be included as part of the response.
65 | * Note: stopSequences is only supported for Gemini models
66 | */
67 | stopSequences?: string[];
68 | /**
69 | * A list of unique `SafetySetting` instances for blocking unsafe content. The API will block
70 | * any prompts and responses that fail to meet the thresholds set by these settings. If there
71 | * is no `SafetySetting` for a given `SafetyCategory` provided in the list, the API will use
72 | * the default safety setting for that category.
73 | */
74 | safetySettings?: SafetySetting[];
75 | /**
76 | * Google API key to use
77 | */
78 | apiKey?: string;
79 | /** Whether to stream the results or not */
80 | streaming?: boolean;
81 | baseURL?: string;
82 | }
83 | /**
84 | * A class that wraps the Google Palm chat model.
85 | * @example
86 | * ```typescript
87 | * const model = new ChatGoogleGenerativeAI({
88 | * apiKey: "",
89 | * temperature: 0.7,
90 | * modelName: "gemini-pro",
91 | * topK: 40,
92 | * topP: 1,
93 | * });
94 | * const questions = [
95 | * new HumanMessage({
96 | * content: [
97 | * {
98 | * type: "text",
99 | * text: "You are a funny assistant that answers in pirate language.",
100 | * },
101 | * {
102 | * type: "text",
103 | * text: "What is your favorite food?",
104 | * },
105 | * ]
106 | * })
107 | * ];
108 | * const res = await model.call(questions);
109 | * console.log({ res });
110 | * ```
111 | */
112 | export declare class ChatGoogleGenerativeAI extends BaseChatModel implements GoogleGenerativeAIChatInput {
113 | static lc_name(): string;
114 | lc_serializable: boolean;
115 | get lc_secrets(): {
116 | [key: string]: string;
117 | } | undefined;
118 | modelName: string;
119 | temperature?: number;
120 | maxOutputTokens?: number;
121 | topP?: number;
122 | topK?: number;
123 | stopSequences: string[];
124 | safetySettings?: SafetySetting[];
125 | apiKey?: string;
126 | baseURL?: string;
127 | streaming: boolean;
128 | private client;
129 | get _isMultimodalModel(): boolean;
130 | constructor(fields?: GoogleGenerativeAIChatInput);
131 | _combineLLMOutput(): never[];
132 | _llmType(): string;
133 | _generate(messages: BaseMessage[], options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun): Promise;
134 | _streamResponseChunks(messages: BaseMessage[], options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun): AsyncGenerator;
135 | }
136 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/embeddings.d.ts:
--------------------------------------------------------------------------------
1 | import type { TaskType } from "@fuyun/generative-ai";
2 | import { Embeddings, EmbeddingsParams } from "@langchain/core/embeddings";
3 | /**
4 | * Interface that extends EmbeddingsParams and defines additional
5 | * parameters specific to the GoogleGenerativeAIEmbeddings class.
6 | */
7 | export interface GoogleGenerativeAIEmbeddingsParams extends EmbeddingsParams {
8 | /**
9 | * Model Name to use
10 | *
11 | * Note: The format must follow the pattern - `{model}`
12 | */
13 | modelName?: string;
14 | /**
15 | * Type of task for which the embedding will be used
16 | *
17 | * Note: currently only supported by `embedding-001` model
18 | */
19 | taskType?: TaskType;
20 | /**
21 | * An optional title for the text. Only applicable when TaskType is
22 | * `RETRIEVAL_DOCUMENT`
23 | *
24 | * Note: currently only supported by `embedding-001` model
25 | */
26 | title?: string;
27 | /**
28 | * Whether to strip new lines from the input text. Default to true
29 | */
30 | stripNewLines?: boolean;
31 | /**
32 | * Google API key to use
33 | */
34 | apiKey?: string;
35 | }
36 | /**
37 | * Class that extends the Embeddings class and provides methods for
38 | * generating embeddings using the Google Palm API.
39 | * @example
40 | * ```typescript
41 | * const model = new GoogleGenerativeAIEmbeddings({
42 | * apiKey: "",
43 | * modelName: "embedding-001",
44 | * });
45 | *
46 | * // Embed a single query
47 | * const res = await model.embedQuery(
48 | * "What would be a good company name for a company that makes colorful socks?"
49 | * );
50 | * console.log({ res });
51 | *
52 | * // Embed multiple documents
53 | * const documentRes = await model.embedDocuments(["Hello world", "Bye bye"]);
54 | * console.log({ documentRes });
55 | * ```
56 | */
57 | export declare class GoogleGenerativeAIEmbeddings extends Embeddings implements GoogleGenerativeAIEmbeddingsParams {
58 | apiKey?: string;
59 | modelName: string;
60 | taskType?: TaskType;
61 | title?: string;
62 | stripNewLines: boolean;
63 | maxBatchSize: number;
64 | private client;
65 | constructor(fields?: GoogleGenerativeAIEmbeddingsParams);
66 | private _convertToContent;
67 | protected _embedQueryContent(text: string): Promise;
68 | protected _embedDocumentsContent(documents: string[]): Promise;
69 | /**
70 | * Method that takes a document as input and returns a promise that
71 | * resolves to an embedding for the document. It calls the _embedText
72 | * method with the document as the input.
73 | * @param document Document for which to generate an embedding.
74 | * @returns Promise that resolves to an embedding for the input document.
75 | */
76 | embedQuery(document: string): Promise;
77 | /**
78 | * Method that takes an array of documents as input and returns a promise
79 | * that resolves to a 2D array of embeddings for each document. It calls
80 | * the _embedText method for each document in the array.
81 | * @param documents Array of documents for which to generate embeddings.
82 | * @returns Promise that resolves to a 2D array of embeddings for each input document.
83 | */
84 | embedDocuments(documents: string[]): Promise;
85 | }
86 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/embeddings.js:
--------------------------------------------------------------------------------
1 | import { GoogleGenerativeAI } from "@fuyun/generative-ai";
2 | import { getEnvironmentVariable } from "@langchain/core/utils/env";
3 | import { Embeddings } from "@langchain/core/embeddings";
4 | import { chunkArray } from "@langchain/core/utils/chunk_array";
5 | /**
6 | * Class that extends the Embeddings class and provides methods for
7 | * generating embeddings using the Google Palm API.
8 | * @example
9 | * ```typescript
10 | * const model = new GoogleGenerativeAIEmbeddings({
11 | * apiKey: "",
12 | * modelName: "embedding-001",
13 | * });
14 | *
15 | * // Embed a single query
16 | * const res = await model.embedQuery(
17 | * "What would be a good company name for a company that makes colorful socks?"
18 | * );
19 | * console.log({ res });
20 | *
21 | * // Embed multiple documents
22 | * const documentRes = await model.embedDocuments(["Hello world", "Bye bye"]);
23 | * console.log({ documentRes });
24 | * ```
25 | */
26 | export class GoogleGenerativeAIEmbeddings extends Embeddings {
27 | constructor(fields) {
28 | super(fields ?? {});
29 | Object.defineProperty(this, "apiKey", {
30 | enumerable: true,
31 | configurable: true,
32 | writable: true,
33 | value: void 0
34 | });
35 | Object.defineProperty(this, "modelName", {
36 | enumerable: true,
37 | configurable: true,
38 | writable: true,
39 | value: "embedding-001"
40 | });
41 | Object.defineProperty(this, "taskType", {
42 | enumerable: true,
43 | configurable: true,
44 | writable: true,
45 | value: void 0
46 | });
47 | Object.defineProperty(this, "title", {
48 | enumerable: true,
49 | configurable: true,
50 | writable: true,
51 | value: void 0
52 | });
53 | Object.defineProperty(this, "stripNewLines", {
54 | enumerable: true,
55 | configurable: true,
56 | writable: true,
57 | value: true
58 | });
59 | Object.defineProperty(this, "maxBatchSize", {
60 | enumerable: true,
61 | configurable: true,
62 | writable: true,
63 | value: 100
64 | }); // Max batch size for embedDocuments set by GenerativeModel client's batchEmbedContents call
65 | Object.defineProperty(this, "client", {
66 | enumerable: true,
67 | configurable: true,
68 | writable: true,
69 | value: void 0
70 | });
71 | this.modelName =
72 | fields?.modelName?.replace(/^models\//, "") ?? this.modelName;
73 | this.taskType = fields?.taskType ?? this.taskType;
74 | this.title = fields?.title ?? this.title;
75 | if (this.title && this.taskType !== "RETRIEVAL_DOCUMENT") {
76 | throw new Error("title can only be sepcified with TaskType.RETRIEVAL_DOCUMENT");
77 | }
78 | this.apiKey = fields?.apiKey ?? getEnvironmentVariable("GOOGLE_API_KEY");
79 | if (!this.apiKey) {
80 | throw new Error("Please set an API key for Google GenerativeAI " +
81 | "in the environmentb variable GOOGLE_API_KEY " +
82 | "or in the `apiKey` field of the " +
83 | "GoogleGenerativeAIEmbeddings constructor");
84 | }
85 | this.client = new GoogleGenerativeAI(this.apiKey).getGenerativeModel({
86 | model: this.modelName,
87 | },
88 | {
89 | baseURL: this.baseURL
90 | });
91 | }
92 | _convertToContent(text) {
93 | const cleanedText = this.stripNewLines ? text.replace(/\n/g, " ") : text;
94 | return {
95 | content: { role: "user", parts: [{ text: cleanedText }] },
96 | taskType: this.taskType,
97 | title: this.title,
98 | };
99 | }
100 | async _embedQueryContent(text) {
101 | const req = this._convertToContent(text);
102 | const res = await this.client.embedContent(req);
103 | return res.embedding.values ?? [];
104 | }
105 | async _embedDocumentsContent(documents) {
106 | const batchEmbedChunks = chunkArray(documents, this.maxBatchSize);
107 | const batchEmbedRequests = batchEmbedChunks.map((chunk) => ({
108 | requests: chunk.map((doc) => this._convertToContent(doc)),
109 | }));
110 | const responses = await Promise.allSettled(batchEmbedRequests.map((req) => this.client.batchEmbedContents(req)));
111 | const embeddings = responses.flatMap((res, idx) => {
112 | if (res.status === "fulfilled") {
113 | return res.value.embeddings.map((e) => e.values || []);
114 | }
115 | else {
116 | return Array(batchEmbedChunks[idx].length).fill([]);
117 | }
118 | });
119 | return embeddings;
120 | }
121 | /**
122 | * Method that takes a document as input and returns a promise that
123 | * resolves to an embedding for the document. It calls the _embedText
124 | * method with the document as the input.
125 | * @param document Document for which to generate an embedding.
126 | * @returns Promise that resolves to an embedding for the input document.
127 | */
128 | embedQuery(document) {
129 | return this.caller.call(this._embedQueryContent.bind(this), document);
130 | }
131 | /**
132 | * Method that takes an array of documents as input and returns a promise
133 | * that resolves to a 2D array of embeddings for each document. It calls
134 | * the _embedText method for each document in the array.
135 | * @param documents Array of documents for which to generate embeddings.
136 | * @returns Promise that resolves to a 2D array of embeddings for each input document.
137 | */
138 | embedDocuments(documents) {
139 | return this.caller.call(this._embedDocumentsContent.bind(this), documents);
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from "./chat_models.js";
2 | export * from "./embeddings.js";
3 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/index.js:
--------------------------------------------------------------------------------
1 | export * from "./chat_models.js";
2 | export * from "./embeddings.js";
3 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/utils.d.ts:
--------------------------------------------------------------------------------
1 | import { EnhancedGenerateContentResponse, Content, Part } from "@fuyun/generative-ai";
2 | import { BaseMessage, MessageContent } from "@langchain/core/messages";
3 | import { ChatGenerationChunk, ChatResult } from "@langchain/core/outputs";
4 | export declare function getMessageAuthor(message: BaseMessage): string;
5 | /**
6 | * Maps a message type to a Google Generative AI chat author.
7 | * @param message The message to map.
8 | * @param model The model to use for mapping.
9 | * @returns The message type mapped to a Google Generative AI chat author.
10 | */
11 | export declare function convertAuthorToRole(author: string): "model" | "user";
12 | export declare function convertMessageContentToParts(content: MessageContent, isMultimodalModel: boolean): Part[];
13 | export declare function convertBaseMessagesToContent(messages: BaseMessage[], isMultimodalModel: boolean): Content[];
14 | export declare function mapGenerateContentResultToChatResult(response: EnhancedGenerateContentResponse): ChatResult;
15 | export declare function convertResponseContentToChatGenerationChunk(response: EnhancedGenerateContentResponse): ChatGenerationChunk | null;
16 |
--------------------------------------------------------------------------------
/server/custom/llm/gemini/utils.js:
--------------------------------------------------------------------------------
1 | import { AIMessage, AIMessageChunk, ChatMessage, isBaseMessage, } from "@langchain/core/messages";
2 | import { ChatGenerationChunk, } from "@langchain/core/outputs";
3 | export function getMessageAuthor(message) {
4 | const type = message._getType();
5 | if (ChatMessage.isInstance(message)) {
6 | return message.role;
7 | }
8 | return message.name ?? type;
9 | }
10 | /**
11 | * Maps a message type to a Google Generative AI chat author.
12 | * @param message The message to map.
13 | * @param model The model to use for mapping.
14 | * @returns The message type mapped to a Google Generative AI chat author.
15 | */
16 | export function convertAuthorToRole(author) {
17 | switch (author) {
18 | /**
19 | * Note: Gemini currently is not supporting system messages
20 | * we will convert them to human messages and merge with following
21 | * */
22 | case "ai":
23 | case "model": // getMessageAuthor returns message.name. code ex.: return message.name ?? type;
24 | return "model";
25 | case "system":
26 | case "human":
27 | return "user";
28 | default:
29 | throw new Error(`Unknown / unsupported author: ${author}`);
30 | }
31 | }
32 | export function convertMessageContentToParts(content, isMultimodalModel) {
33 | if (typeof content === "string") {
34 | return [{ text: content }];
35 | }
36 | return content.map((c) => {
37 | if (c.type === "text") {
38 | return {
39 | text: c.text,
40 | };
41 | }
42 | if (c.type === "image_url") {
43 | if (!isMultimodalModel) {
44 | throw new Error(`This model does not support images`);
45 | }
46 | if (typeof c.image_url !== "string") {
47 | throw new Error("Please provide image as base64 encoded data URL");
48 | }
49 | const [dm, data] = c.image_url.split(",");
50 | if (!dm.startsWith("data:")) {
51 | throw new Error("Please provide image as base64 encoded data URL");
52 | }
53 | const [mimeType, encoding] = dm.replace(/^data:/, "").split(";");
54 | if (encoding !== "base64") {
55 | throw new Error("Please provide image as base64 encoded data URL");
56 | }
57 | return {
58 | inlineData: {
59 | data,
60 | mimeType,
61 | },
62 | };
63 | }
64 | throw new Error(`Unknown content type ${c.type}`);
65 | });
66 | }
67 | export function convertBaseMessagesToContent(messages, isMultimodalModel) {
68 | return messages.reduce((acc, message, index) => {
69 | if (!isBaseMessage(message)) {
70 | throw new Error("Unsupported message input");
71 | }
72 | const author = getMessageAuthor(message);
73 | if (author === "system" && index !== 0) {
74 | throw new Error("System message should be the first one");
75 | }
76 | const role = convertAuthorToRole(author);
77 | const prevContent = acc.content[acc.content.length];
78 | if (!acc.mergeWithPreviousContent &&
79 | prevContent &&
80 | prevContent.role === role) {
81 | throw new Error("Google Generative AI requires alternate messages between authors");
82 | }
83 | const parts = convertMessageContentToParts(message.content, isMultimodalModel);
84 | if (acc.mergeWithPreviousContent) {
85 | const prevContent = acc.content[acc.content.length - 1];
86 | if (!prevContent) {
87 | throw new Error("There was a problem parsing your system message. Please try a prompt without one.");
88 | }
89 | prevContent.parts.push(...parts);
90 | return {
91 | mergeWithPreviousContent: false,
92 | content: acc.content,
93 | };
94 | }
95 | const content = {
96 | role,
97 | parts,
98 | };
99 | return {
100 | mergeWithPreviousContent: author === "system",
101 | content: [...acc.content, content],
102 | };
103 | }, { content: [], mergeWithPreviousContent: false }).content;
104 | }
105 | export function mapGenerateContentResultToChatResult(response) {
106 | // if rejected or error, return empty generations with reason in filters
107 | if (!response.candidates ||
108 | response.candidates.length === 0 ||
109 | !response.candidates[0]) {
110 | return {
111 | generations: [],
112 | llmOutput: {
113 | filters: response.promptFeedback,
114 | },
115 | };
116 | }
117 | const [candidate] = response.candidates;
118 | const { content, ...generationInfo } = candidate;
119 | const text = content?.parts[0]?.text ?? "";
120 | const generation = {
121 | text,
122 | message: new AIMessage({
123 | content: text,
124 | name: !content ? undefined : content.role,
125 | additional_kwargs: generationInfo,
126 | }),
127 | generationInfo,
128 | };
129 | return {
130 | generations: [generation],
131 | };
132 | }
133 | export function convertResponseContentToChatGenerationChunk(response) {
134 | if (!response.candidates || response.candidates.length === 0) {
135 | return null;
136 | }
137 | const [candidate] = response.candidates;
138 | const { content, ...generationInfo } = candidate;
139 | const text = content?.parts[0]?.text ?? "";
140 | return new ChatGenerationChunk({
141 | text,
142 | message: new AIMessageChunk({
143 | content: text,
144 | name: !content ? undefined : content.role,
145 | // Each chunk can have unique "generationInfo", and merging strategy is unclear,
146 | // so leave blank for now.
147 | additional_kwargs: {},
148 | }),
149 | generationInfo,
150 | });
151 | }
152 |
--------------------------------------------------------------------------------
/server/custom/tools/dalle/dalle.d.ts:
--------------------------------------------------------------------------------
1 | import { OpenAI as OpenAIClient } from "openai";
2 | import { Tool, ToolParams } from "@langchain/core/tools";
3 | /**
4 | * An interface for the Dall-E API Wrapper.
5 | */
6 | export interface DallEAPIWrapperParams extends ToolParams {
7 | baseURL?: string;
8 | /**
9 | * The OpenAI API key
10 | */
11 | openAIApiKey?: string;
12 | /**
13 | * The model to use.
14 | * @params "dall-e-2" | "dall-e-3"
15 | * @default "dall-e-3"
16 | */
17 | modelName?: string;
18 | /**
19 | * The style of the generated images. Must be one of vivid or natural.
20 | * Vivid causes the model to lean towards generating hyper-real and dramatic images.
21 | * Natural causes the model to produce more natural, less hyper-real looking images.
22 | * @default "vivid"
23 | */
24 | style?: "natural" | "vivid";
25 | /**
26 | * The quality of the image that will be generated. ‘hd’ creates images with finer
27 | * details and greater consistency across the image.
28 | * @default "standard"
29 | */
30 | quality?: "standard" | "hd";
31 | /**
32 | * The number of images to generate.
33 | * Must be between 1 and 10.
34 | * For dall-e-3, only `n: 1` is supported.
35 | * @default 1
36 | */
37 | n?: number;
38 | /**
39 | * The size of the generated images.
40 | * Must be one of 256x256, 512x512, or 1024x1024 for DALL·E-2 models.
41 | * Must be one of 1024x1024, 1792x1024, or 1024x1792 for DALL·E-3 models.
42 | * @default "1024x1024"
43 | */
44 | size?: "256x256" | "512x512" | "1024x1024" | "1792x1024" | "1024x1792";
45 | /**
46 | * The format in which the generated images are returned.
47 | * Must be one of "url" or "b64_json".
48 | * @default "url"
49 | */
50 | responseFormat?: "url" | "b64_json";
51 | /**
52 | * A unique identifier representing your end-user, which will help
53 | * OpenAI to monitor and detect abuse.
54 | */
55 | user?: string;
56 | /**
57 | * The organization to use
58 | */
59 | organization?: string;
60 | }
61 | /**
62 | * A tool for generating images with Open AIs Dall-E 2 or 3 API.
63 | */
64 | export declare class DallEAPIWrapper extends Tool {
65 | static lc_name(): string;
66 | name: string;
67 | description: string;
68 | protected client: OpenAIClient;
69 | static readonly toolName = "dalle_api_wrapper";
70 | private modelName;
71 | private style;
72 | private quality;
73 | private n;
74 | private size;
75 | private responseFormat;
76 | private user?;
77 | constructor(fields?: DallEAPIWrapperParams);
78 | /** @ignore */
79 | _call(input: string): Promise;
80 | }
81 |
--------------------------------------------------------------------------------
/server/custom/tools/dalle/dalle.js:
--------------------------------------------------------------------------------
1 | import { getEnvironmentVariable } from "@langchain/core/utils/env";
2 | import { OpenAI as OpenAIClient } from "openai";
3 | import { Tool } from "@langchain/core/tools";
4 | /**
5 | * A tool for generating images with Open AIs Dall-E 2 or 3 API.
6 | */
7 | export class DallEAPIWrapper extends Tool {
8 | static lc_name() {
9 | return "DallEAPIWrapper";
10 | }
11 | constructor(fields) {
12 | super(fields);
13 | Object.defineProperty(this, "name", {
14 | enumerable: true,
15 | configurable: true,
16 | writable: true,
17 | value: "dalle_api_wrapper"
18 | });
19 | Object.defineProperty(this, "description", {
20 | enumerable: true,
21 | configurable: true,
22 | writable: true,
23 | value: "A wrapper around OpenAI DALL-E API. Useful for when you need to generate images from a text description. Input should be an image description.plz output the full original url ended with params"
24 | });
25 | Object.defineProperty(this, "client", {
26 | enumerable: true,
27 | configurable: true,
28 | writable: true,
29 | value: void 0
30 | });
31 | Object.defineProperty(this, "modelName", {
32 | enumerable: true,
33 | configurable: true,
34 | writable: true,
35 | value: "dall-e-3"
36 | });
37 | Object.defineProperty(this, "style", {
38 | enumerable: true,
39 | configurable: true,
40 | writable: true,
41 | value: "vivid"
42 | });
43 | Object.defineProperty(this, "quality", {
44 | enumerable: true,
45 | configurable: true,
46 | writable: true,
47 | value: "standard"
48 | });
49 | Object.defineProperty(this, "n", {
50 | enumerable: true,
51 | configurable: true,
52 | writable: true,
53 | value: 1
54 | });
55 | Object.defineProperty(this, "size", {
56 | enumerable: true,
57 | configurable: true,
58 | writable: true,
59 | value: "1024x1024"
60 | });
61 | Object.defineProperty(this, "responseFormat", {
62 | enumerable: true,
63 | configurable: true,
64 | writable: true,
65 | value: "url"
66 | });
67 | Object.defineProperty(this, "user", {
68 | enumerable: true,
69 | configurable: true,
70 | writable: true,
71 | value: void 0
72 | });
73 | const openAIApiKey = fields?.openAIApiKey ?? getEnvironmentVariable("OPENAI_API_KEY");
74 | const organization = fields?.organization ?? getEnvironmentVariable("OPENAI_ORGANIZATION");
75 | const clientConfig = {
76 | baseURL: fields?.baseURL ?? "https://api.openai.com/v1",
77 | apiKey: openAIApiKey,
78 | organization,
79 | dangerouslyAllowBrowser: true,
80 | };
81 | this.client = new OpenAIClient(clientConfig);
82 | this.modelName = fields?.modelName ?? this.modelName;
83 | this.style = fields?.style ?? this.style;
84 | this.quality = fields?.quality ?? this.quality;
85 | this.n = fields?.n ?? this.n;
86 | this.size = fields?.size ?? this.size;
87 | this.responseFormat = fields?.responseFormat ?? this.responseFormat;
88 | this.user = fields?.user;
89 | }
90 | /** @ignore */
91 | async _call(input) {
92 | const response = await this.client.images.generate({
93 | model: this.modelName,
94 | prompt: input,
95 | n: this.n,
96 | size: this.size,
97 | response_format: this.responseFormat,
98 | style: this.style,
99 | quality: this.quality,
100 | user: this.user,
101 | });
102 | let data = "";
103 | if (this.responseFormat === "url") {
104 | [data] = response.data
105 | .map((item) => item.url)
106 | .filter((url) => url !== "undefined");
107 | console.log(data)
108 | }
109 | else {
110 | [data] = response.data
111 | .map((item) => item.b64_json)
112 | .filter((b64_json) => b64_json !== "undefined");
113 | }
114 | return data;
115 | }
116 | }
117 | Object.defineProperty(DallEAPIWrapper, "toolName", {
118 | enumerable: true,
119 | configurable: true,
120 | writable: true,
121 | value: "dalle_api_wrapper"
122 | });
123 |
--------------------------------------------------------------------------------
/server/server.ts:
--------------------------------------------------------------------------------
1 | import Fastify from 'fastify';
2 | import cors from '@fastify/cors';
3 | import { Chat } from './agent';
4 |
5 | const fastify = Fastify({
6 | logger: true,
7 | });
8 |
9 | fastify.register(cors,{
10 | origin: '*',
11 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
12 | });
13 |
14 | fastify.post('/api/chat', async (request, reply) => {
15 | return Chat(request.body);
16 | });
17 |
18 | // Start server
19 | fastify.listen({ port: 6677 }, (err, address) => {
20 | // console.log(`Server listening at ${address}`);
21 | if (err) {
22 | throw err
23 | }
24 | });
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "jsx": "preserve",
7 | "lib": [
8 | "dom",
9 | "es2017"
10 | ],
11 | "moduleResolution": "node",
12 | "allowJs": true,
13 | "noEmit": true,
14 | "strict": true,
15 | "allowSyntheticDefaultImports": true,
16 | "skipLibCheck": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "removeComments": false,
20 | "preserveConstEnums": true,
21 | "sourceMap": true,
22 | "esModuleInterop": true,
23 | "forceConsistentCasingInFileNames": true,
24 | "resolveJsonModule": true,
25 | "isolatedModules": true,
26 | "incremental": true,
27 | "paths": {
28 | "@/*": ["./*"]
29 | },
30 | "plugins": [
31 | {
32 | "name": "next"
33 | }
34 | ]
35 | },
36 | "exclude": [
37 | "node_modules",
38 | "src-tauri"
39 | ],
40 | "include": [
41 | "next-env.d.ts",
42 | "**/*.ts",
43 | "**/*.tsx",
44 | "../dist/types/**/*.ts",
45 | ".next/types/**/*.ts"
46 | , "../app/api/reactAgent/route.ts" ]
47 | }
48 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | default-run = "app"
9 | edition = "2021"
10 | rust-version = "1.60"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.5.1", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0"
19 | serde = { version = "1.0", features = ["derive"] }
20 | tauri = { version = "1.6.1", features = [ "os-all", "shell-open", "shell-sidecar"] }
21 |
22 | [features]
23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
24 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
25 | # DO NOT REMOVE!!
26 | custom-protocol = [ "tauri/custom-protocol" ]
27 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/etrobot/next-langchain-tauri/fffbcdf1da5c71fb18eb444a3f0b5853011e9517/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | use tauri::{
5 | Builder,
6 | api::process::Command,
7 | generate_context,
8 | };
9 |
10 | fn main() {
11 | Builder::default()
12 | .setup(|_app| {
13 | let _ = Command::new_sidecar("server")
14 | .expect("failed to create `my-sidecar` binary command")
15 | .spawn()
16 | .expect("Failed to spawn sidecar");
17 | Ok(())
18 | })
19 | .run(generate_context!())
20 | .expect("error while running tauri application");
21 | }
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "pnpm build && pnpm pkg-server",
5 | "beforeDevCommand": "pnpm dev",
6 | "devPath": "http://localhost:3100",
7 | "distDir": "../out"
8 | },
9 | "package": {
10 | "productName": "Next-Langchain-Tauri",
11 | "version": "0.3.0"
12 | },
13 | "tauri": {
14 | "allowlist": {
15 | "all": false,
16 | "shell": {
17 | "open": true,
18 | "sidecar": true,
19 | "scope": [
20 | { "name": "bin/server", "sidecar": true }
21 | ]
22 | },
23 | "os": {
24 | "all": true
25 | }
26 | },
27 | "bundle": {
28 | "active": true,
29 | "category": "DeveloperTool",
30 | "copyright": "",
31 | "deb": {
32 | "depends": []
33 | },
34 | "externalBin": ["bin/server"],
35 | "icon": [
36 | "icons/32x32.png",
37 | "icons/128x128.png",
38 | "icons/128x128@2x.png",
39 | "icons/icon.icns",
40 | "icons/icon.ico"
41 | ],
42 | "identifier": "com.stackai.nextlang",
43 | "longDescription": "",
44 | "macOS": {
45 | "entitlements": null,
46 | "exceptionDomain": "",
47 | "frameworks": [],
48 | "providerShortName": null,
49 | "signingIdentity": null
50 | },
51 | "resources": [],
52 | "shortDescription": "",
53 | "targets": "all",
54 | "windows": {
55 | "certificateThumbprint": null,
56 | "digestAlgorithm": "sha256",
57 | "timestampUrl": ""
58 | }
59 |
60 | },
61 | "security": {
62 | "csp": null
63 | },
64 | "updater": {
65 | "active": false
66 | },
67 | "windows": [
68 | {
69 | "fullscreen": false,
70 | "height": 800,
71 | "resizable": true,
72 | "title": "Next-Langchain-Tauri",
73 | "width": 1280
74 | }
75 | ]
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ['class'],
4 | content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
5 | theme: {
6 | container: {
7 | center: true,
8 | padding: '2rem',
9 | screens: {
10 | '2xl': '1400px'
11 | }
12 | },
13 | extend: {
14 | fontFamily: {
15 | sans: ['var(--font-geist-sans)',],
16 | mono: ['var(--font-geist-mono)']
17 | },
18 | colors: {
19 | border: 'hsl(var(--border))',
20 | input: 'hsl(var(--input))',
21 | ring: 'hsl(var(--ring))',
22 | background: 'hsl(var(--background))',
23 | foreground: 'hsl(var(--foreground))',
24 | primary: {
25 | DEFAULT: 'hsl(var(--primary))',
26 | foreground: 'hsl(var(--primary-foreground))'
27 | },
28 | secondary: {
29 | DEFAULT: 'hsl(var(--secondary))',
30 | foreground: 'hsl(var(--secondary-foreground))'
31 | },
32 | destructive: {
33 | DEFAULT: 'hsl(var(--destructive))',
34 | foreground: 'hsl(var(--destructive-foreground))'
35 | },
36 | muted: {
37 | DEFAULT: 'hsl(var(--muted))',
38 | foreground: 'hsl(var(--muted-foreground))'
39 | },
40 | accent: {
41 | DEFAULT: 'hsl(var(--accent))',
42 | foreground: 'hsl(var(--accent-foreground))'
43 | },
44 | popover: {
45 | DEFAULT: 'hsl(var(--popover))',
46 | foreground: 'hsl(var(--popover-foreground))'
47 | },
48 | card: {
49 | DEFAULT: 'hsl(var(--card))',
50 | foreground: 'hsl(var(--card-foreground))'
51 | }
52 | },
53 | borderRadius: {
54 | lg: `var(--radius)`,
55 | md: `calc(var(--radius) - 2px)`,
56 | sm: 'calc(var(--radius) - 4px)'
57 | },
58 | keyframes: {
59 | 'accordion-down': {
60 | from: { height: 0 },
61 | to: { height: 'var(--radix-accordion-content-height)' }
62 | },
63 | 'accordion-up': {
64 | from: { height: 'var(--radix-accordion-content-height)' },
65 | to: { height: 0 }
66 | },
67 | 'slide-from-left': {
68 | '0%': {
69 | transform: 'translateX(-100%)'
70 | },
71 | '100%': {
72 | transform: 'translateX(0)'
73 | }
74 | },
75 | 'slide-to-left': {
76 | '0%': {
77 | transform: 'translateX(0)'
78 | },
79 | '100%': {
80 | transform: 'translateX(-100%)'
81 | }
82 | }
83 | },
84 | animation: {
85 | 'slide-from-left':
86 | 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
87 | 'slide-to-left':
88 | 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
89 | 'accordion-down': 'accordion-down 0.2s ease-out',
90 | 'accordion-up': 'accordion-up 0.2s ease-out'
91 | }
92 | }
93 | },
94 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
95 | }
96 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "forceConsistentCasingInFileNames": true,
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "bundler",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules", "src-tauri"]
28 | }
29 |
--------------------------------------------------------------------------------