├── .oxlintrc.json ├── .npmrc ├── .tool-versions ├── .dockerignore ├── src ├── tools │ ├── index.ts │ └── image.ts ├── components │ ├── ui │ │ ├── input │ │ │ ├── index.ts │ │ │ └── Input.vue │ │ ├── label │ │ │ ├── index.ts │ │ │ └── Label.vue │ │ ├── textarea │ │ │ ├── index.ts │ │ │ └── Textarea.vue │ │ ├── tabs │ │ │ ├── index.ts │ │ │ ├── Tabs.vue │ │ │ ├── TabsList.vue │ │ │ ├── TabsContent.vue │ │ │ └── TabsTrigger.vue │ │ ├── dialog │ │ │ ├── DialogClose.vue │ │ │ ├── DialogTrigger.vue │ │ │ ├── DialogHeader.vue │ │ │ ├── DialogFooter.vue │ │ │ ├── Dialog.vue │ │ │ ├── index.ts │ │ │ ├── DialogDescription.vue │ │ │ ├── DialogTitle.vue │ │ │ ├── DialogScrollContent.vue │ │ │ └── DialogContent.vue │ │ ├── select │ │ │ ├── SelectValue.vue │ │ │ ├── SelectItemText.vue │ │ │ ├── Select.vue │ │ │ ├── SelectLabel.vue │ │ │ ├── SelectSeparator.vue │ │ │ ├── SelectGroup.vue │ │ │ ├── index.ts │ │ │ ├── SelectScrollUpButton.vue │ │ │ ├── SelectScrollDownButton.vue │ │ │ ├── SelectTrigger.vue │ │ │ ├── SelectItem.vue │ │ │ └── SelectContent.vue │ │ └── button │ │ │ ├── Button.vue │ │ │ └── index.ts │ ├── nodes │ │ ├── Node.vue │ │ ├── SystemNode.vue │ │ ├── UserNode.vue │ │ └── AssistantNode.vue │ ├── SystemPromptEdit.vue │ ├── ConversationNodeContextMenu.vue │ ├── NodeContextMenu.vue │ ├── SystemPrompt.vue │ ├── MarkdownTable.vue │ ├── MarkdownCodeBlock.vue │ ├── MarkdownView.vue │ ├── ModelSelector.vue │ └── ConversationView.vue ├── types │ ├── embedding.ts │ ├── templates.ts │ ├── node.ts │ ├── settings.ts │ ├── rooms.ts │ ├── messages.ts │ └── tutorial.ts ├── types.ts ├── utils │ ├── prompts.ts │ ├── index.ts │ ├── interator.ts │ ├── chat.ts │ ├── markdown │ │ ├── remarkCaptureRaw.test.ts │ │ └── remarkCaptureRaw.ts │ ├── prompt.md │ ├── chat.test.ts │ └── tutorial.ts ├── utils.ts ├── composables │ ├── dark.ts │ └── useLayout.ts ├── modules │ ├── pinia.ts │ ├── nprogress.ts │ ├── pwa.ts │ └── i18n.ts ├── pages │ ├── [...all].vue │ ├── index.vue │ └── settings │ │ └── modules │ │ └── text-generation.vue ├── stores │ ├── mode.ts │ ├── settings.ts │ ├── database.ts │ ├── messages.ts │ ├── rooms.ts │ └── tutorial.ts ├── styles │ ├── markdown.css │ └── main.css ├── events │ └── embedding-worker.ts ├── shims.d.ts ├── layouts │ ├── 404.vue │ └── default.vue ├── main.ts ├── models │ ├── template.ts │ ├── rooms.ts │ ├── messages.test.ts │ └── messages.ts ├── typed-router.d.ts ├── workers │ └── embedding-worker.ts └── App.vue ├── flow-chat-demo.png ├── public ├── _headers ├── pwa-192x192.png ├── pwa-512x512.png ├── favicon.svg ├── favicon-dark.svg └── safari-pinned-tab.svg ├── pnpm-workspace.yaml ├── cspell.config.yaml ├── drizzle ├── 0002_sleepy_warpath.sql ├── 0001_chubby_odin.sql ├── meta │ ├── _journal.json │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ └── 0002_snapshot.json └── 0000_goofy_rocket_racer.sql ├── drizzle.config.ts ├── .editorconfig ├── .gitignore ├── vitest.config.ts ├── eslint.config.ts ├── locales ├── zh-CN.yml └── en.yml ├── netlify.toml ├── .github ├── actions │ └── setup-pnpm │ │ └── action.yaml └── workflows │ └── ci.yaml ├── .cursor └── rules │ └── flow-chat.mdc ├── components.json ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── README.zh-CN.md ├── index.html ├── tsconfig.json ├── LICENSE ├── README.md ├── uno.config.ts ├── db └── schema.ts ├── vite.config.ts └── package.json /.oxlintrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 24.0.2 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './image' 2 | -------------------------------------------------------------------------------- /src/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /src/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /flow-chat-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonNekoGH/flow-chat/HEAD/flow-chat-demo.png -------------------------------------------------------------------------------- /src/components/ui/textarea/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Textarea } from './Textarea.vue' 2 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable 4 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonNekoGH/flow-chat/HEAD/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LemonNekoGH/flow-chat/HEAD/public/pwa-512x512.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | - simple-git-hooks 4 | - vue-demi 5 | - esbuild 6 | -------------------------------------------------------------------------------- /src/types/embedding.ts: -------------------------------------------------------------------------------- 1 | export interface EmbeddingParams { 2 | text: string[] | string 3 | instruction?: string 4 | } 5 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | words: 2 | - airi 3 | - duckdb 4 | - pinia 5 | - rehype 6 | - reka-ui 7 | - vue-sonner 8 | - xsai 9 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { Router } from 'vue-router' 3 | 4 | export type UserModule = (app: App, router: Router) => void 5 | -------------------------------------------------------------------------------- /src/types/templates.ts: -------------------------------------------------------------------------------- 1 | export interface Template { 2 | id: string 3 | name: string 4 | system_prompt: string 5 | created_at: Date 6 | updated_at: Date 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/prompts.ts: -------------------------------------------------------------------------------- 1 | export const SUMMARY_PROMPT = `Summarize the following content concisely, briefly describe the content with at most 100 words, and use bulletins when needed:` 2 | -------------------------------------------------------------------------------- /drizzle/0002_sleepy_warpath.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "messages" ADD COLUMN "summary" text;--> statement-breakpoint 2 | ALTER TABLE "messages" ADD COLUMN "show_summary" boolean DEFAULT false NOT NULL; -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit' 2 | 3 | export default defineConfig({ 4 | dialect: 'postgresql', 5 | schema: './db/schema.ts', 6 | out: './drizzle', 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | *.local 5 | dist 6 | dist-ssr 7 | node_modules 8 | .idea/ 9 | *.log 10 | cypress/downloads 11 | public/assets/fonts 12 | .cache 13 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from 'clsx' 2 | import { clsx } from 'clsx' 3 | import { twMerge } from 'tailwind-merge' 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | -------------------------------------------------------------------------------- /src/composables/dark.ts: -------------------------------------------------------------------------------- 1 | import { useDark, usePreferredDark, useToggle } from '@vueuse/core' 2 | 3 | export const isDark = useDark() 4 | export const toggleDark = useToggle(isDark) 5 | export const preferredDark = usePreferredDark() 6 | -------------------------------------------------------------------------------- /src/types/node.ts: -------------------------------------------------------------------------------- 1 | import type { Message } from './messages' 2 | 3 | export interface NodeData { 4 | message: Message 5 | selected: boolean 6 | inactive: boolean 7 | hidden: boolean 8 | generating: boolean 9 | } 10 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config' 2 | import viteConfig from './vite.config' 3 | 4 | export default mergeConfig(viteConfig, defineConfig({ 5 | test: { 6 | include: ['src/**/*.test.ts'], 7 | }, 8 | })) 9 | -------------------------------------------------------------------------------- /src/components/ui/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tabs } from './Tabs.vue' 2 | export { default as TabsContent } from './TabsContent.vue' 3 | export { default as TabsList } from './TabsList.vue' 4 | export { default as TabsTrigger } from './TabsTrigger.vue' 5 | -------------------------------------------------------------------------------- /src/modules/pinia.ts: -------------------------------------------------------------------------------- 1 | import type { UserModule } from '~/types' 2 | import { createPinia } from 'pinia' 3 | 4 | // Setup Pinia 5 | // https://pinia.vuejs.org/ 6 | export const install: UserModule = (app) => { 7 | const pinia = createPinia() 8 | app.use(pinia) 9 | } 10 | -------------------------------------------------------------------------------- /src/types/settings.ts: -------------------------------------------------------------------------------- 1 | export interface Provider { 2 | id: `${string}-${string}-${string}-${string}-${string}` 3 | name: string 4 | apiKey: string 5 | baseURL: string 6 | } 7 | 8 | export interface ModelInfo { 9 | provider: string 10 | model: string 11 | } 12 | -------------------------------------------------------------------------------- /drizzle/0001_chubby_odin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "messages" DROP CONSTRAINT "messages_room_id_rooms_id_fk"; 2 | --> statement-breakpoint 3 | ALTER TABLE "messages" ADD CONSTRAINT "messages_room_id_rooms_id_fk" FOREIGN KEY ("room_id") REFERENCES "public"."rooms"("id") ON DELETE cascade ON UPDATE no action; -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | import oxlint from 'eslint-plugin-oxlint' 3 | 4 | export default antfu( 5 | { 6 | unocss: true, 7 | formatters: true, 8 | markdown: true, 9 | }, 10 | ...oxlint.buildFromOxlintConfigFile('./.oxlintrc.json'), 11 | ) 12 | -------------------------------------------------------------------------------- /locales/zh-CN.yml: -------------------------------------------------------------------------------- 1 | button: 2 | about: 关于 3 | back: 返回 4 | go: 确定 5 | home: 首页 6 | toggle_dark: 切换深色模式 7 | toggle_langs: 切换语言 8 | intro: 9 | desc: 固执己见的 Vite 项目模板 10 | dynamic-route: 动态路由演示 11 | hi: 你好,{name} 12 | aka: 也叫 13 | whats-your-name: 输入你的名字 14 | not-found: 未找到页面 15 | -------------------------------------------------------------------------------- /src/pages/[...all].vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | meta: 15 | layout: 404 16 | 17 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/types/rooms.ts: -------------------------------------------------------------------------------- 1 | export interface Room { 2 | id: string 3 | name: string 4 | template_id: string | null 5 | default_model: string | null 6 | focus_node_id: string | null 7 | viewport_x: number | null 8 | viewport_y: number | null 9 | viewport_zoom: number | null 10 | created_at: Date 11 | updated_at: Date 12 | } 13 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "pnpm run build" 4 | 5 | [build.environment] 6 | NODE_VERSION = "23" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | 13 | [[headers]] 14 | for = "/manifest.webmanifest" 15 | 16 | [headers.values] 17 | Content-Type = "application/manifest+json" 18 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/modules/nprogress.ts: -------------------------------------------------------------------------------- 1 | import type { UserModule } from '~/types' 2 | import NProgress from 'nprogress' 3 | 4 | export const install: UserModule = (_, router) => { 5 | router.beforeEach((to, from) => { 6 | if (to.path !== from.path) 7 | NProgress.start() 8 | }) 9 | router.afterEach(() => { 10 | NProgress.done() 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/stores/mode.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export enum ChatMode { 5 | FLOW = 'flow', 6 | CONVERSATION = 'conversation', 7 | } 8 | 9 | export const useModeStore = defineStore('mode', () => { 10 | const currentMode = ref(ChatMode.FLOW) 11 | 12 | return { 13 | currentMode, 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | button: 2 | about: About 3 | back: Back 4 | go: GO 5 | home: Home 6 | toggle_dark: Toggle dark mode 7 | toggle_langs: Change languages 8 | intro: 9 | desc: Opinionated Vite Starter Template 10 | dynamic-route: Demo of dynamic route 11 | hi: Hi, {name}! 12 | aka: Also known as 13 | whats-your-name: What's your name? 14 | not-found: Not found 15 | -------------------------------------------------------------------------------- /.github/actions/setup-pnpm/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup pnpm 2 | description: Private action to setup pnpm 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: pnpm/action-setup@v3 8 | - uses: actions/setup-node@v4 9 | with: 10 | node-version: 24.0.2 11 | cache: pnpm 12 | 13 | - name: Install 14 | shell: bash 15 | run: pnpm install 16 | -------------------------------------------------------------------------------- /.cursor/rules/flow-chat.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # FlowChat 7 | 8 | [README.md](mdc:README.md) 9 | 10 | [package.json](mdc:package.json) 11 | 12 | ## Rules 13 | 14 | - Components here `components/ui`, if you want to use shadcn/vue, just use the cli command to install instead of create component directly. 15 | - Stores here `stores/` 16 | - 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://shadcn-vue.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/styles/main.css", 7 | "baseColor": "neutral", 8 | "cssVariables": true, 9 | "prefix": "" 10 | }, 11 | "aliases": { 12 | "components": "~/components", 13 | "utils": "~/utils" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.unocss", 5 | "antfu.goto-alias", 6 | "csstools.postcss", 7 | "dbaeumer.vscode-eslint", 8 | "vue.volar", 9 | "lokalise.i18n-ally", 10 | "EditorConfig.EditorConfig", 11 | "streetsidesoftware.code-spell-checker", 12 | "yzhang.markdown-all-in-one", 13 | "usernamehw.errorlens" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/modules/pwa.ts: -------------------------------------------------------------------------------- 1 | import type { UserModule } from '~/types' 2 | 3 | // https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available 4 | export const install: UserModule = (_, router) => { 5 | router.isReady() 6 | .then(async () => { 7 | const { registerSW } = await import('virtual:pwa-register') 8 | registerSW({ immediate: true }) 9 | }) 10 | .catch(() => {}) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/nodes/Node.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /src/components/ui/tabs/Tabs.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine as build-stage 2 | 3 | WORKDIR /app 4 | RUN corepack enable 5 | 6 | COPY .npmrc package.json pnpm-lock.yaml ./ 7 | RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \ 8 | pnpm install --frozen-lockfile 9 | 10 | COPY . . 11 | RUN pnpm build 12 | 13 | FROM nginx:stable-alpine as production-stage 14 | 15 | COPY --from=build-stage /app/dist /usr/share/nginx/html 16 | EXPOSE 80 17 | 18 | CMD ["nginx", "-g", "daemon off;"] 19 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from 'clsx' 2 | import { clsx } from 'clsx' 3 | import { twMerge } from 'tailwind-merge' 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export function setToggle(s: Set, t: T) { 10 | s.has(t) ? s.delete(t) : s.add(t) 11 | } 12 | 13 | export function scrollToBottom(el?: Element | null) { 14 | if (el) { 15 | el.scrollTop = el.scrollHeight 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/types/messages.ts: -------------------------------------------------------------------------------- 1 | export type MessageRole = 'user' | 'assistant' | 'system' 2 | 3 | export interface BaseMessage { 4 | content: string 5 | role: MessageRole 6 | } 7 | 8 | export interface Message extends BaseMessage { 9 | id: string 10 | parent_id: string | null 11 | room_id: string | null 12 | provider: string // provider used to generate this message 13 | model: string // model used to generate this message 14 | summary?: string 15 | show_summary?: boolean 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/markdown.css: -------------------------------------------------------------------------------- 1 | .prose pre:not(.shiki) { 2 | padding: 0; 3 | } 4 | 5 | .prose .shiki { 6 | font-family: 'DM Mono', monospace; 7 | font-size: 1.2em; 8 | line-height: 1.4; 9 | } 10 | 11 | .prose img { 12 | width: 100%; 13 | } 14 | 15 | .shiki, 16 | .shiki span { 17 | color: var(--shiki-light); 18 | background: var(--shiki-light-bg); 19 | } 20 | 21 | html.dark .shiki, 22 | html.dark .shiki span { 23 | color: var(--shiki-dark); 24 | background: var(--shiki-dark-bg); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /src/events/embedding-worker.ts: -------------------------------------------------------------------------------- 1 | import type { EmbeddingParams } from '~/types/embedding' 2 | import { defineEventa, defineInvokeEventa } from '@moeru/eventa' 3 | 4 | export const embeddingLoadModelInvoke = defineInvokeEventa('embedding:eventa:invoke:load-model') 5 | export const embeddingExtractInvoke = defineInvokeEventa, EmbeddingParams>('embedding:eventa:invoke:extract') 6 | 7 | export const embeddingModelLoadingProgressEvent = defineEventa('embedding:eventa:event:model-load-progress') 8 | -------------------------------------------------------------------------------- /src/components/nodes/SystemNode.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | // extend the window 3 | } 4 | 5 | // with unplugin-vue-markdown, markdown files can be treated as Vue components 6 | declare module '*.md' { 7 | import type { DefineComponent } from 'vue' 8 | 9 | const component: DefineComponent 10 | export default component 11 | } 12 | 13 | declare module '*.vue' { 14 | import type { DefineComponent } from 'vue' 15 | 16 | const component: DefineComponent 17 | export default component 18 | } 19 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Flow Chat 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/641805a6-407e-4af2-a66f-06385f146717/deploy-status)](https://app.netlify.com/sites/flow-chat/deploys) 4 | 5 | 多分支 LLM 对话 UI。[Demo](https://flow-chat.lemonneko.moe/) 6 | 7 | ![demo](./flow-chat-demo.png) 8 | 9 | ## 特性 10 | 11 | - 文本生成:基本的聊天功能 12 | - 图片生成:从文本生成图片 13 | - 分支:从消息创建新分支 14 | - 模型切换:在同一个对话中切换不同的模型 15 | 16 | ## 开发 17 | 18 | ```bash 19 | pnpm install 20 | pnpm dev 21 | ``` 22 | 23 | ## 构建 24 | 25 | ```bash 26 | pnpm build 27 | ``` 28 | -------------------------------------------------------------------------------- /src/layouts/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /src/utils/interator.ts: -------------------------------------------------------------------------------- 1 | export async function* asyncIteratorFromReadableStream( 2 | res: ReadableStream, 3 | func: (value: F) => Promise, 4 | ): AsyncGenerator { 5 | const reader = res.getReader() 6 | try { 7 | while (true) { 8 | const { done, value } = await reader.read() 9 | if (done) { 10 | return 11 | } 12 | 13 | yield func(value) 14 | } 15 | } 16 | finally { 17 | try { 18 | await reader.cancel() 19 | } 20 | catch { 21 | } 22 | reader.releaseLock() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1765033772031, 9 | "tag": "0000_goofy_rocket_racer", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1765725699989, 16 | "tag": "0001_chubby_odin", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1765949623337, 23 | "tag": "0002_sleepy_warpath", 24 | "breakpoints": true 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/types/tutorial.ts: -------------------------------------------------------------------------------- 1 | import type { RemovableRef } from '@vueuse/core' 2 | import type { Driver, DriveStep } from 'driver.js' 3 | import type { Ref } from 'vue' 4 | 5 | export interface Tutorial { 6 | localStorageKey: string 7 | showSkip: Ref 8 | // use setSteps() instead of create a new instance, https://github.com/kamranahmedse/driver.js/issues/464#issuecomment-2716673766 9 | steps: DriveStep[] 10 | isFirstHere: RemovableRef 11 | 12 | onCloseClick: (element: Element | undefined, step: DriveStep, { driver }: { driver: Driver }) => void 13 | goToStep: (stepTitle: string) => void 14 | setConfig: () => void 15 | } 16 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /src/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue' 2 | export { default as SelectContent } from './SelectContent.vue' 3 | export { default as SelectGroup } from './SelectGroup.vue' 4 | export { default as SelectItem } from './SelectItem.vue' 5 | export { default as SelectItemText } from './SelectItemText.vue' 6 | export { default as SelectLabel } from './SelectLabel.vue' 7 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue' 8 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue' 9 | export { default as SelectSeparator } from './SelectSeparator.vue' 10 | export { default as SelectTrigger } from './SelectTrigger.vue' 11 | export { default as SelectValue } from './SelectValue.vue' 12 | -------------------------------------------------------------------------------- /src/components/ui/tabs/TabsList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 29 | -------------------------------------------------------------------------------- /src/components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /src/components/ui/tabs/TabsContent.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /src/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/components/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/components/nodes/UserNode.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/ui/textarea/Textarea.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 |