asasvirtuais 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/actions/draw.ts +110 -0
- package/components/OAuthCard.tsx +346 -0
- package/components/icons.tsx +11 -0
- package/components/markdown.tsx +18 -0
- package/components/stack/list.tsx +21 -0
- package/components/stack/menu.tsx +40 -0
- package/components/stack/nav.tsx +39 -0
- package/components/table/fixed.tsx +59 -0
- package/components/table/key-value.tsx +19 -0
- package/components/ui/color-mode.tsx +108 -0
- package/components/ui/provider.tsx +15 -0
- package/components/ui/toaster.tsx +43 -0
- package/components/ui/tooltip.tsx +46 -0
- package/hooks/useBoolean.tsx +11 -0
- package/hooks/useForwardAs.tsx +29 -0
- package/hooks/useHash copy.tsx +27 -0
- package/hooks/useHash.tsx +27 -0
- package/hooks/useIsMobile.tsx +6 -0
- package/hooks/useOAuthTokens.ts +97 -0
- package/hooks/useOpenRouterModels.ts +80 -0
- package/lib/auth0.ts +11 -0
- package/lib/blob.ts +3 -0
- package/lib/client-token-storage.ts +216 -0
- package/lib/oauth-tokens.ts +85 -0
- package/lib/react/context.tsx +20 -0
- package/lib/react/index.ts +1 -0
- package/lib/tools.ts +375 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +23 -0
- package/package.json +72 -0
- package/packages/blob.ts +97 -0
- package/packages/chat/components/chat/feed/index.tsx +76 -0
- package/packages/chat/components/chat/feed/story.tsx +18 -0
- package/packages/chat/components/chat/index.tsx +16 -0
- package/packages/chat/components/chat/story.tsx +74 -0
- package/packages/chat/components/debug/index.tsx +54 -0
- package/packages/chat/components/header/index.tsx +14 -0
- package/packages/chat/components/header/menu/index.tsx +63 -0
- package/packages/chat/components/header/story.tsx +33 -0
- package/packages/chat/components/header/title/index.tsx +35 -0
- package/packages/chat/components/index.ts +13 -0
- package/packages/chat/components/input/index.tsx +17 -0
- package/packages/chat/components/input/menu/index.tsx +35 -0
- package/packages/chat/components/input/send.tsx +21 -0
- package/packages/chat/components/input/story.tsx +35 -0
- package/packages/chat/components/input/textarea/index.tsx +20 -0
- package/packages/chat/components/message/file.tsx +103 -0
- package/packages/chat/components/message/menu/index.tsx +26 -0
- package/packages/chat/components/message/story.tsx +49 -0
- package/packages/chat/components/messages/index.tsx +23 -0
- package/packages/chat/components/messages/story.tsx +11 -0
- package/packages/chat/components/ui/prose.tsx +263 -0
- package/packages/chat/edit-message.tsx +49 -0
- package/packages/chat/header.tsx +118 -0
- package/packages/chat/index.ts +14 -0
- package/packages/chat/input.tsx +89 -0
- package/packages/chat/message-menu.tsx +57 -0
- package/packages/chat/message.tsx +44 -0
- package/packages/chat/messages.tsx +44 -0
- package/packages/chat/model-selector.tsx +172 -0
- package/packages/chat/scenarios.tsx +68 -0
- package/packages/chat/settings.tsx +98 -0
- package/packages/chat/temperature-slider.tsx +67 -0
- package/packages/chat/tool-results.tsx +32 -0
- package/packages/crud/core.ts +75 -0
- package/packages/crud/fetcher.ts +64 -0
- package/packages/crud/index.ts +2 -0
- package/packages/crud/next.ts +128 -0
- package/packages/crud/react.tsx +365 -0
- package/packages/env.ts +8 -0
- package/packages/fields.tsx +157 -0
- package/packages/firebase.ts +13 -0
- package/packages/firestore.ts +51 -0
- package/packages/form.tsx +66 -0
- package/packages/next.ts +64 -0
- package/packages/openrouter.ts +4 -0
- package/packages/react/context.tsx +21 -0
- package/packages/react/crud.tsx +372 -0
- package/packages/react/hooks.ts +90 -0
- package/packages/react/store.tsx +20 -0
- package/packages/replit-db.ts +219 -0
- package/packages/wretch.ts +22 -0
- package/packages/yaml.ts +163 -0
- package/pnpm-workspace.yaml +4 -0
- package/server/db.ts +15 -0
package/next-env.d.ts
ADDED
package/next.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NextConfig } from 'next'
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
reactStrictMode: false,
|
|
5
|
+
eslint: { ignoreDuringBuilds: true },
|
|
6
|
+
typescript: { ignoreBuildErrors: true },
|
|
7
|
+
experimental: {
|
|
8
|
+
optimizePackageImports: ['@chakra-ui/react'],
|
|
9
|
+
serverActions: {
|
|
10
|
+
allowedOrigins: [
|
|
11
|
+
'localhost:5000',
|
|
12
|
+
'localhost:3000',
|
|
13
|
+
'congenial-halibut-x9g7497q747hvw9x.github.dev',
|
|
14
|
+
'congenial-halibut-x9g7497q747hvw9x-3000.app.github.dev',
|
|
15
|
+
'*.app.github.dev',
|
|
16
|
+
'*.replit.dev',
|
|
17
|
+
'*.repl.co'
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default nextConfig
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "asasvirtuais",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"directories": {
|
|
5
|
+
"packages": "./packages"
|
|
6
|
+
},
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
"./*": "./packages/*",
|
|
10
|
+
"./*/*": "./packages/*/*"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"code": "tmux new-session -d 'termux-wake-lock && sh dev-tunnel.sh'",
|
|
14
|
+
"dev": "next dev --port 5000 --hostname 0.0.0.0",
|
|
15
|
+
"build": "next build",
|
|
16
|
+
"start": "next start",
|
|
17
|
+
"lint": "next lint",
|
|
18
|
+
"claude": "claude --dangerously-skip-permissions --mcp-config ./mcp.json",
|
|
19
|
+
"gemini": "gemini --yolo --all_files",
|
|
20
|
+
"env:pull": "vercel env pull .env"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@ai-sdk/react": "^2.0.49",
|
|
24
|
+
"@asasvirtuais/airtable": "github:asasvirtuais/airtable#main",
|
|
25
|
+
"@asasvirtuais/chat": "github:asasvirtuais/chat#main",
|
|
26
|
+
"@asasvirtuais/crud": "github:asasvirtuais/crud#main",
|
|
27
|
+
"@asasvirtuais/react": "github:asasvirtuais/react#main",
|
|
28
|
+
"@auth0/nextjs-auth0": "^4.10.0",
|
|
29
|
+
"@chakra-ui/react": "^3.21.0",
|
|
30
|
+
"@emotion/react": "^11.14.0",
|
|
31
|
+
"@feathersjs/adapter-commons": "^5.0.35",
|
|
32
|
+
"@feathersjs/feathers": "^5.0.35",
|
|
33
|
+
"@feathersjs/knex": "^5.0.35",
|
|
34
|
+
"@google-cloud/storage": "^7.16.0",
|
|
35
|
+
"@openrouter/ai-sdk-provider": "^1.0.0-beta.1",
|
|
36
|
+
"ai": "^5.0.49",
|
|
37
|
+
"axios": "^1.10.0",
|
|
38
|
+
"date-fns": "^4.1.0",
|
|
39
|
+
"firebase": "^12.3.0",
|
|
40
|
+
"firebase-admin": "^13.5.0",
|
|
41
|
+
"google-auth-library": "^10.1.0",
|
|
42
|
+
"googleapis": "^153.0.0",
|
|
43
|
+
"jszip": "^3.10.1",
|
|
44
|
+
"knex": "^3.1.0",
|
|
45
|
+
"next": "15.3.4",
|
|
46
|
+
"next-themes": "^0.4.6",
|
|
47
|
+
"openai": "^5.9.0",
|
|
48
|
+
"oracledb": "^6.9.0",
|
|
49
|
+
"pg": "^8.16.3",
|
|
50
|
+
"pg-query-stream": "^4.10.3",
|
|
51
|
+
"react": "^19.0.0",
|
|
52
|
+
"react-dom": "^19.0.0",
|
|
53
|
+
"react-icons": "^5.5.0",
|
|
54
|
+
"react-markdown": "^10.1.0",
|
|
55
|
+
"react-use": "^17.6.0",
|
|
56
|
+
"remark-breaks": "^4.0.0",
|
|
57
|
+
"remark-gfm": "^4.0.1",
|
|
58
|
+
"satori": "^0.18.3",
|
|
59
|
+
"search-params": "^4.0.1",
|
|
60
|
+
"wretch": "^2.11.0",
|
|
61
|
+
"zod": "^3.25.76"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@anthropic-ai/claude-code": "^1.0.60",
|
|
65
|
+
"@google/gemini-cli": "^0.1.9",
|
|
66
|
+
"@types/node": "^20",
|
|
67
|
+
"@types/react": "^19",
|
|
68
|
+
"@types/react-dom": "^19",
|
|
69
|
+
"typescript": "^5",
|
|
70
|
+
"vercel": "^44.3.0"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/packages/blob.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Storage } from '@google-cloud/storage'
|
|
2
|
+
import { promises as fs } from 'fs'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
// bucket name hardcoded as requested by the user
|
|
7
|
+
const bucketName = 'asasvirtuais.firebasestorage.app'
|
|
8
|
+
|
|
9
|
+
let storage: Storage | undefined
|
|
10
|
+
|
|
11
|
+
function initStorage() {
|
|
12
|
+
if (storage) return storage
|
|
13
|
+
if (process.env.SERVICE_ACCOUNT_JSON) {
|
|
14
|
+
const raw = process.env.SERVICE_ACCOUNT_JSON
|
|
15
|
+
try {
|
|
16
|
+
// Treat the env var as the JSON content of the service account
|
|
17
|
+
const creds = JSON.parse(raw)
|
|
18
|
+
storage = new Storage({ projectId: creds.project_id, credentials: creds })
|
|
19
|
+
return storage
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// If parsing fails, throw a helpful error so the developer can fix the env var
|
|
22
|
+
throw new Error('Invalid GOOGLE_APPLICATION_CREDENTIALS: expected JSON content. ' + (e instanceof Error ? e.message : String(e)))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback: use application default credentials
|
|
27
|
+
storage = new Storage()
|
|
28
|
+
return storage
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function uploadBuffer(buffer: Buffer, filename: string, contentType?: string) {
|
|
32
|
+
if (!bucketName) {
|
|
33
|
+
throw new Error('No bucket configured. Set VERCEL_BLOB_BUCKET or GCLOUD_STORAGE_BUCKET env var.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const gcs = initStorage()
|
|
37
|
+
const bucket = gcs.bucket(bucketName)
|
|
38
|
+
const file = bucket.file(filename)
|
|
39
|
+
// Create a write stream and write the buffer. This is more robust in some
|
|
40
|
+
// serverless environments where stream lifecycle can be sensitive.
|
|
41
|
+
const stream = file.createWriteStream({
|
|
42
|
+
resumable: false,
|
|
43
|
+
metadata: {
|
|
44
|
+
contentType: contentType || 'application/octet-stream',
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await new Promise<void>((resolve, reject) => {
|
|
50
|
+
stream.on('finish', () => resolve())
|
|
51
|
+
stream.on('error', (err) => reject(err))
|
|
52
|
+
stream.end(buffer)
|
|
53
|
+
})
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
// If the stream was destroyed (ERR_STREAM_DESTROYED), fallback to writing a
|
|
56
|
+
// temp file and using bucket.upload which is often more robust in some
|
|
57
|
+
// serverless environments.
|
|
58
|
+
console.warn('stream write failed, attempting tmp-file fallback', err)
|
|
59
|
+
if (err && err.code === 'ERR_STREAM_DESTROYED') {
|
|
60
|
+
const tmpDir = os.tmpdir()
|
|
61
|
+
const tmpPath = path.join(tmpDir, `${Date.now()}-${filename}`)
|
|
62
|
+
try {
|
|
63
|
+
await fs.writeFile(tmpPath, buffer)
|
|
64
|
+
await bucket.upload(tmpPath, { destination: filename, metadata: { contentType: contentType || 'application/octet-stream' } })
|
|
65
|
+
} finally {
|
|
66
|
+
try { await fs.unlink(tmpPath) } catch (e) { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
throw err
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Make the file public (best-effort). If your bucket is private, switch to signed URLs.
|
|
74
|
+
try {
|
|
75
|
+
await file.makePublic()
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.warn('makePublic failed', e)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `https://storage.googleapis.com/${bucketName}/${encodeURIComponent(filename)}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function deleteFile(filename: string) {
|
|
84
|
+
if (!bucketName) return
|
|
85
|
+
const gcs = initStorage()
|
|
86
|
+
const bucket = gcs.bucket(bucketName)
|
|
87
|
+
const file = bucket.file(filename)
|
|
88
|
+
await file.delete({ ignoreNotFound: true })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function getPublicUrl(filename: string) {
|
|
92
|
+
if (!bucketName) return null
|
|
93
|
+
return `https://storage.googleapis.com/${bucketName}/${encodeURIComponent(filename)}`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Note: This uses Google Cloud Storage. If you want to switch to Vercel Blob,
|
|
97
|
+
// replace the implementation and provide the appropriate Vercel credentials.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { Box, CloseButton, Editable, Flex, Grid, GridItem, GridItemProps, Heading, HStack, IconButton, Menu, useBreakpointValue, useDisclosure } from '@chakra-ui/react'
|
|
4
|
+
import { ChatHeader } from '../../header'
|
|
5
|
+
import { HeaderTitle } from '../../header/title'
|
|
6
|
+
import { HeaderMenu } from '../../header/menu'
|
|
7
|
+
|
|
8
|
+
export function Main({ children, ...props } : GridItemProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Box h='100%' {...props}>
|
|
11
|
+
{children}
|
|
12
|
+
</Box>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Feed({ children, ...props } : GridItemProps) {
|
|
17
|
+
return (
|
|
18
|
+
<Box h='100%' {...props}>
|
|
19
|
+
{children}
|
|
20
|
+
</Box>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { RiSideBarLine } from 'react-icons/ri'
|
|
25
|
+
import { ChatLayout } from '../../chat'
|
|
26
|
+
import ChatMessages from '../../messages'
|
|
27
|
+
import ChatInput from '../../input'
|
|
28
|
+
import InputTextarea from '../../input/textarea'
|
|
29
|
+
import { InputSend } from '../../input/send'
|
|
30
|
+
import InputMenu from '../../input/menu'
|
|
31
|
+
|
|
32
|
+
export function ChatFeedLayout() {
|
|
33
|
+
const { open, onOpen, onClose } = useDisclosure()
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Flex h='100dvh' w='100dvw'>
|
|
37
|
+
<Main w={open ? '0px' : '100dvw'} maxW={{base: '100%', md: '66.66%'}} overflow='hidden'>
|
|
38
|
+
<ChatLayout>
|
|
39
|
+
<ChatHeader>
|
|
40
|
+
<HeaderTitle value='My Scenario' />
|
|
41
|
+
<IconButton display={{base: 'block', md: 'none'}} onClick={onOpen} variant='plain'>
|
|
42
|
+
<RiSideBarLine/>
|
|
43
|
+
</IconButton>
|
|
44
|
+
<HeaderMenu>
|
|
45
|
+
<Menu.Item value='settings'>Settings <Menu.ItemCommand>⚙️</Menu.ItemCommand></Menu.Item>
|
|
46
|
+
<Menu.Separator/>
|
|
47
|
+
<Menu.Item value='scenarios'>My Scenarios <Menu.ItemCommand>🎭</Menu.ItemCommand></Menu.Item>
|
|
48
|
+
<Menu.Separator/>
|
|
49
|
+
<Menu.Item value='new-scenario'>New Scenario <Menu.ItemCommand>➕</Menu.ItemCommand></Menu.Item>
|
|
50
|
+
<Menu.Separator/>
|
|
51
|
+
<Menu.Item value='delete-scenario' color='red.500'>Delete Scenario <Menu.ItemCommand>❌</Menu.ItemCommand></Menu.Item>
|
|
52
|
+
</HeaderMenu>
|
|
53
|
+
</ChatHeader>
|
|
54
|
+
<ChatMessages>
|
|
55
|
+
<Editable.Root placeholder='System instructions'>
|
|
56
|
+
<Editable.Preview/>
|
|
57
|
+
<Editable.Textarea/>
|
|
58
|
+
</Editable.Root>
|
|
59
|
+
</ChatMessages>
|
|
60
|
+
<ChatInput>
|
|
61
|
+
<InputTextarea/>
|
|
62
|
+
<InputSend/>
|
|
63
|
+
</ChatInput>
|
|
64
|
+
</ChatLayout>
|
|
65
|
+
</Main>
|
|
66
|
+
<Feed w={open ? '100dvw' : '0px'} maxW={{base: '100%', md: '33.33%'}} overflow='hidden'>
|
|
67
|
+
<CloseButton m={4} onClick={onClose} />
|
|
68
|
+
<Box p={4}>
|
|
69
|
+
<Heading>Feed</Heading>
|
|
70
|
+
</Box>
|
|
71
|
+
</Feed>
|
|
72
|
+
</Flex>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default ChatFeedLayout
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { ChatFeedLayout } from "../feed"
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ChatFeedLayout> = {
|
|
6
|
+
title: "Components/Chat",
|
|
7
|
+
component: ChatFeedLayout,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default meta
|
|
11
|
+
type Story = StoryObj<typeof meta>
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export const ChatFeed: Story = {
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: "fullscreen",
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { SimpleGrid, Center } from '@chakra-ui/react'
|
|
4
|
+
|
|
5
|
+
export function ChatLayout({ children }: React.PropsWithChildren) {
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Center h='100dvh'>
|
|
9
|
+
<SimpleGrid gridTemplateRows='auto 1fr auto' h='100dvh' w='100%' p={2} mx='auto'>
|
|
10
|
+
{children}
|
|
11
|
+
</SimpleGrid>
|
|
12
|
+
</Center>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default ChatLayout
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
+
import { ChatLayout } from "./index"
|
|
4
|
+
import { Code, Menu, MenuItem } from "@chakra-ui/react"
|
|
5
|
+
import ChatHeader from "../header"
|
|
6
|
+
import HeaderMenu from "../header/menu"
|
|
7
|
+
import HeaderTitle from "../header/title"
|
|
8
|
+
import ChatInput from "../input"
|
|
9
|
+
import InputMenu from "../input/menu"
|
|
10
|
+
import { InputSend } from "../input/send"
|
|
11
|
+
import InputTextarea from "../input/textarea"
|
|
12
|
+
import ChatMessages from "../messages"
|
|
13
|
+
import ChatMessage from "../message"
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof ChatLayout> = {
|
|
16
|
+
title: "Components/Chat",
|
|
17
|
+
component: ChatLayout,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default meta
|
|
21
|
+
type Story = StoryObj<typeof meta>
|
|
22
|
+
|
|
23
|
+
export const Chat: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
children: (
|
|
26
|
+
<>
|
|
27
|
+
<ChatHeader>
|
|
28
|
+
<HeaderTitle value="Chat Title" />
|
|
29
|
+
<HeaderMenu>
|
|
30
|
+
<Menu.Item>Menu item 1</Menu.Item>
|
|
31
|
+
<Menu.Item>Menu item 2</Menu.Item>
|
|
32
|
+
<Menu.Item>Menu item 3</Menu.Item>
|
|
33
|
+
</HeaderMenu>
|
|
34
|
+
</ChatHeader>
|
|
35
|
+
<ChatMessages>
|
|
36
|
+
<ChatMessage
|
|
37
|
+
avatar=""
|
|
38
|
+
username="user"
|
|
39
|
+
menu={
|
|
40
|
+
<Menu.Root>
|
|
41
|
+
<MenuItem>Menu item 1</MenuItem>
|
|
42
|
+
<MenuItem>Menu item 2</MenuItem>
|
|
43
|
+
<MenuItem>Menu item 3</MenuItem>
|
|
44
|
+
</Menu.Root>
|
|
45
|
+
}
|
|
46
|
+
info={
|
|
47
|
+
<Code>
|
|
48
|
+
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
|
|
49
|
+
Corrupti distinctio vel vero, atque fuga nihil voluptas vitae
|
|
50
|
+
obcaecati dignissimos, nostrum omnis veniam dolorem? Incidunt
|
|
51
|
+
itaque eos possimus dolorem totam?
|
|
52
|
+
</Code>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
### Lorem ipsum dolor sit amet consectetur adipisicing elit.
|
|
56
|
+
Cupiditate corporis nisi saepe pariatur, error fuga aperiam.
|
|
57
|
+
Dignissimos harum consectetur et quam facere pariatur quos ea
|
|
58
|
+
voluptatum laudantium omnis voluptatem rerum quibusdam odio sequi
|
|
59
|
+
voluptates, quisquam inventore earum!
|
|
60
|
+
</ChatMessage>
|
|
61
|
+
</ChatMessages>
|
|
62
|
+
<ChatInput>
|
|
63
|
+
<InputMenu>
|
|
64
|
+
<MenuItem>Menu item 1</MenuItem>
|
|
65
|
+
<MenuItem>Menu item 2</MenuItem>
|
|
66
|
+
<MenuItem>Menu item 3</MenuItem>
|
|
67
|
+
</InputMenu>
|
|
68
|
+
<InputTextarea />
|
|
69
|
+
<InputSend />
|
|
70
|
+
</ChatInput>
|
|
71
|
+
</>
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import { Badge, Box, HStack, Text, VStack, Tag } from '@chakra-ui/react'
|
|
3
|
+
import { LuClock, LuZap } from 'react-icons/lu'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
|
|
6
|
+
interface DebugData {
|
|
7
|
+
model?: string
|
|
8
|
+
provider?: string
|
|
9
|
+
usage?: {
|
|
10
|
+
promptTokens: number
|
|
11
|
+
completionTokens: number
|
|
12
|
+
totalTokens: number
|
|
13
|
+
}
|
|
14
|
+
duration?: number
|
|
15
|
+
temperature?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DebugInfoProps {
|
|
19
|
+
log: DebugData
|
|
20
|
+
compact?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DebugInfo: React.FC<DebugInfoProps> = ({ log, compact = false }) => {
|
|
24
|
+
return (
|
|
25
|
+
<Box p={3} bg="gray.50" rounded="md" fontSize="xs">
|
|
26
|
+
<HStack justify="space-between" mb={2}>
|
|
27
|
+
<Badge colorScheme="green">Complete</Badge>
|
|
28
|
+
{log.model && <Text color="gray.600">{log.model}</Text>}
|
|
29
|
+
</HStack>
|
|
30
|
+
|
|
31
|
+
<HStack gap={4}>
|
|
32
|
+
{log.duration && (
|
|
33
|
+
<HStack gap={1}>
|
|
34
|
+
<LuClock size={12} />
|
|
35
|
+
<Text>{log.duration}</Text>
|
|
36
|
+
</HStack>
|
|
37
|
+
)}
|
|
38
|
+
|
|
39
|
+
{log.usage?.totalTokens && (
|
|
40
|
+
<HStack gap={1}>
|
|
41
|
+
<LuZap size={12} />
|
|
42
|
+
<Text>{log.usage.totalTokens} tokens</Text>
|
|
43
|
+
</HStack>
|
|
44
|
+
)}
|
|
45
|
+
|
|
46
|
+
{log.temperature && (
|
|
47
|
+
<Text color="gray.500">T: {log.temperature}</Text>
|
|
48
|
+
)}
|
|
49
|
+
</HStack>
|
|
50
|
+
</Box>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default DebugInfo
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { GridItem, HStack } from '@chakra-ui/react'
|
|
3
|
+
|
|
4
|
+
export function ChatHeader( { children } : React.PropsWithChildren ) {
|
|
5
|
+
return (
|
|
6
|
+
<GridItem as='header'>
|
|
7
|
+
<HStack w='full' justifyContent='space-between'>
|
|
8
|
+
{children}
|
|
9
|
+
</HStack>
|
|
10
|
+
</GridItem>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default ChatHeader
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useMemo, useCallback } from "react"
|
|
3
|
+
import {
|
|
4
|
+
IconButton,
|
|
5
|
+
Menu,
|
|
6
|
+
Portal,
|
|
7
|
+
HStack,
|
|
8
|
+
useBreakpointValue,
|
|
9
|
+
} from "@chakra-ui/react"
|
|
10
|
+
import { RiSideBarLine } from "react-icons/ri"
|
|
11
|
+
|
|
12
|
+
interface HeaderMenuProps extends React.PropsWithChildren {
|
|
13
|
+
onToggleFeed?: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function HeaderMenu({ children, onToggleFeed }: HeaderMenuProps) {
|
|
17
|
+
const isMobile = useBreakpointValue({ base: true, md: false })
|
|
18
|
+
|
|
19
|
+
const showMobileFeedButton = useMemo(
|
|
20
|
+
() => isMobile && onToggleFeed,
|
|
21
|
+
[isMobile, onToggleFeed]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const handleToggleFeed = useCallback(() => {
|
|
25
|
+
onToggleFeed?.()
|
|
26
|
+
}, [onToggleFeed])
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<HStack>
|
|
30
|
+
{/* Feed toggle button - only shown on mobile */}
|
|
31
|
+
{showMobileFeedButton && (
|
|
32
|
+
<IconButton
|
|
33
|
+
aria-label="toggle-feed"
|
|
34
|
+
variant="ghost"
|
|
35
|
+
size="lg"
|
|
36
|
+
onClick={handleToggleFeed}
|
|
37
|
+
>
|
|
38
|
+
<RiSideBarLine />
|
|
39
|
+
</IconButton>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<Menu.Root closeOnSelect={false}>
|
|
43
|
+
<Menu.Trigger asChild>
|
|
44
|
+
<IconButton
|
|
45
|
+
aria-label="open-menu"
|
|
46
|
+
fontSize="3xl"
|
|
47
|
+
variant="ghost"
|
|
48
|
+
size="lg"
|
|
49
|
+
>
|
|
50
|
+
⋮
|
|
51
|
+
</IconButton>
|
|
52
|
+
</Menu.Trigger>
|
|
53
|
+
<Portal>
|
|
54
|
+
<Menu.Positioner>
|
|
55
|
+
<Menu.Content>{children}</Menu.Content>
|
|
56
|
+
</Menu.Positioner>
|
|
57
|
+
</Portal>
|
|
58
|
+
</Menu.Root>
|
|
59
|
+
</HStack>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default HeaderMenu
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
3
|
+
import { MenuItem } from '@chakra-ui/react'
|
|
4
|
+
import { ChatHeader } from './index'
|
|
5
|
+
import HeaderTitle from './title'
|
|
6
|
+
import HeaderMenu from './menu'
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof ChatHeader> = {
|
|
9
|
+
title: 'Components/Header',
|
|
10
|
+
component: ChatHeader,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'centered',
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default meta
|
|
17
|
+
|
|
18
|
+
type Story = StoryObj<typeof meta>
|
|
19
|
+
|
|
20
|
+
export const Header: Story = {
|
|
21
|
+
args: {
|
|
22
|
+
children: (
|
|
23
|
+
<>
|
|
24
|
+
<HeaderTitle value='Chat Title' />
|
|
25
|
+
<HeaderMenu>
|
|
26
|
+
<MenuItem>Menu item 1</MenuItem>
|
|
27
|
+
<MenuItem>Menu item 2</MenuItem>
|
|
28
|
+
<MenuItem>Menu item 3</MenuItem>
|
|
29
|
+
</HeaderMenu>
|
|
30
|
+
</>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { Editable, HStack } from '@chakra-ui/react'
|
|
4
|
+
|
|
5
|
+
export function HeaderTitle({
|
|
6
|
+
onChange,
|
|
7
|
+
value,
|
|
8
|
+
defaultValue,
|
|
9
|
+
}: {
|
|
10
|
+
onChange?: (value: string) => void,
|
|
11
|
+
value?: string
|
|
12
|
+
defaultValue?: string
|
|
13
|
+
}) {
|
|
14
|
+
const [title, setTitle] = React.useState(value)
|
|
15
|
+
useEffect(() => setTitle(value), [value])
|
|
16
|
+
useEffect(() => setTitle(defaultValue), [defaultValue])
|
|
17
|
+
return (
|
|
18
|
+
<Editable.Root
|
|
19
|
+
justifyContent='flex-start'
|
|
20
|
+
size='lg'
|
|
21
|
+
fontSize='xl'
|
|
22
|
+
my={2}
|
|
23
|
+
placeholder='Untitled'
|
|
24
|
+
value={title}
|
|
25
|
+
defaultValue={defaultValue}
|
|
26
|
+
onValueChange={({value}) => setTitle(value)}
|
|
27
|
+
onValueCommit={({value}) => onChange?.(value)}
|
|
28
|
+
>
|
|
29
|
+
<Editable.Preview />
|
|
30
|
+
<Editable.Input />
|
|
31
|
+
</Editable.Root>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default HeaderTitle
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as ChatLayout } from './chat'
|
|
2
|
+
export { ChatFeedLayout, Main, Feed } from './chat/feed'
|
|
3
|
+
export { default as ChatHeader } from './header'
|
|
4
|
+
export { default as HeaderTitle } from './header/title'
|
|
5
|
+
export { default as HeaderMenu } from './header/menu'
|
|
6
|
+
export { default as ChatInput } from './input'
|
|
7
|
+
export { default as InputMenu } from './input/menu'
|
|
8
|
+
export { InputSend } from './input/send'
|
|
9
|
+
export { default as InputTextarea } from './input/textarea'
|
|
10
|
+
export { default as MessageMenu } from './message/menu'
|
|
11
|
+
export { default as ChatMessages } from './messages'
|
|
12
|
+
export { Prose } from './ui/prose'
|
|
13
|
+
export { DebugInfo } from './debug'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { GridItem, Stack, HStack, StackProps } from '@chakra-ui/react'
|
|
4
|
+
|
|
5
|
+
export function ChatInput({ children, ...props }: StackProps) {
|
|
6
|
+
return (
|
|
7
|
+
<GridItem as='footer' w='full'>
|
|
8
|
+
<Stack w='full' gap={1}>
|
|
9
|
+
<HStack w='full' alignItems='flex-end' {...props}>
|
|
10
|
+
{children}
|
|
11
|
+
</HStack>
|
|
12
|
+
</Stack>
|
|
13
|
+
</GridItem>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default ChatInput
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Icon, IconButton, Menu, Portal, Stack } from '@chakra-ui/react'
|
|
3
|
+
import { BsFillPlusCircleFill } from 'react-icons/bs'
|
|
4
|
+
|
|
5
|
+
export function InputMenu( { children } : React.PropsWithChildren ) {
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Menu.Root closeOnSelect={false}>
|
|
9
|
+
{/* @ts-expect-error dunno */}
|
|
10
|
+
<Menu.Trigger asChild>
|
|
11
|
+
<IconButton
|
|
12
|
+
aria-label='Open menu'
|
|
13
|
+
size='md'
|
|
14
|
+
variant='plain'
|
|
15
|
+
>
|
|
16
|
+
<Icon size='xl' asChild>
|
|
17
|
+
<BsFillPlusCircleFill />
|
|
18
|
+
</Icon>
|
|
19
|
+
</IconButton>
|
|
20
|
+
|
|
21
|
+
</Menu.Trigger>
|
|
22
|
+
<Portal>
|
|
23
|
+
<Menu.Positioner>
|
|
24
|
+
<Menu.Content>
|
|
25
|
+
<Stack>
|
|
26
|
+
{children}
|
|
27
|
+
</Stack>
|
|
28
|
+
</Menu.Content>
|
|
29
|
+
</Menu.Positioner>
|
|
30
|
+
</Portal>
|
|
31
|
+
</Menu.Root>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default InputMenu
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { IconButton, IconButtonProps, Spinner } from '@chakra-ui/react'
|
|
3
|
+
|
|
4
|
+
interface InputSendProps extends IconButtonProps {
|
|
5
|
+
loading?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function InputSend({ loading, ...props }: InputSendProps) {
|
|
9
|
+
return (
|
|
10
|
+
<IconButton
|
|
11
|
+
aria-label='Send message'
|
|
12
|
+
fontSize='2xl'
|
|
13
|
+
variant='ghost'
|
|
14
|
+
size='md'
|
|
15
|
+
disabled={loading || props.disabled}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
{loading ? <Spinner size="sm" /> : '➤'}
|
|
19
|
+
</IconButton>
|
|
20
|
+
)
|
|
21
|
+
}
|