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.
Files changed (86) hide show
  1. package/README.md +78 -0
  2. package/actions/draw.ts +110 -0
  3. package/components/OAuthCard.tsx +346 -0
  4. package/components/icons.tsx +11 -0
  5. package/components/markdown.tsx +18 -0
  6. package/components/stack/list.tsx +21 -0
  7. package/components/stack/menu.tsx +40 -0
  8. package/components/stack/nav.tsx +39 -0
  9. package/components/table/fixed.tsx +59 -0
  10. package/components/table/key-value.tsx +19 -0
  11. package/components/ui/color-mode.tsx +108 -0
  12. package/components/ui/provider.tsx +15 -0
  13. package/components/ui/toaster.tsx +43 -0
  14. package/components/ui/tooltip.tsx +46 -0
  15. package/hooks/useBoolean.tsx +11 -0
  16. package/hooks/useForwardAs.tsx +29 -0
  17. package/hooks/useHash copy.tsx +27 -0
  18. package/hooks/useHash.tsx +27 -0
  19. package/hooks/useIsMobile.tsx +6 -0
  20. package/hooks/useOAuthTokens.ts +97 -0
  21. package/hooks/useOpenRouterModels.ts +80 -0
  22. package/lib/auth0.ts +11 -0
  23. package/lib/blob.ts +3 -0
  24. package/lib/client-token-storage.ts +216 -0
  25. package/lib/oauth-tokens.ts +85 -0
  26. package/lib/react/context.tsx +20 -0
  27. package/lib/react/index.ts +1 -0
  28. package/lib/tools.ts +375 -0
  29. package/next-env.d.ts +5 -0
  30. package/next.config.ts +23 -0
  31. package/package.json +72 -0
  32. package/packages/blob.ts +97 -0
  33. package/packages/chat/components/chat/feed/index.tsx +76 -0
  34. package/packages/chat/components/chat/feed/story.tsx +18 -0
  35. package/packages/chat/components/chat/index.tsx +16 -0
  36. package/packages/chat/components/chat/story.tsx +74 -0
  37. package/packages/chat/components/debug/index.tsx +54 -0
  38. package/packages/chat/components/header/index.tsx +14 -0
  39. package/packages/chat/components/header/menu/index.tsx +63 -0
  40. package/packages/chat/components/header/story.tsx +33 -0
  41. package/packages/chat/components/header/title/index.tsx +35 -0
  42. package/packages/chat/components/index.ts +13 -0
  43. package/packages/chat/components/input/index.tsx +17 -0
  44. package/packages/chat/components/input/menu/index.tsx +35 -0
  45. package/packages/chat/components/input/send.tsx +21 -0
  46. package/packages/chat/components/input/story.tsx +35 -0
  47. package/packages/chat/components/input/textarea/index.tsx +20 -0
  48. package/packages/chat/components/message/file.tsx +103 -0
  49. package/packages/chat/components/message/menu/index.tsx +26 -0
  50. package/packages/chat/components/message/story.tsx +49 -0
  51. package/packages/chat/components/messages/index.tsx +23 -0
  52. package/packages/chat/components/messages/story.tsx +11 -0
  53. package/packages/chat/components/ui/prose.tsx +263 -0
  54. package/packages/chat/edit-message.tsx +49 -0
  55. package/packages/chat/header.tsx +118 -0
  56. package/packages/chat/index.ts +14 -0
  57. package/packages/chat/input.tsx +89 -0
  58. package/packages/chat/message-menu.tsx +57 -0
  59. package/packages/chat/message.tsx +44 -0
  60. package/packages/chat/messages.tsx +44 -0
  61. package/packages/chat/model-selector.tsx +172 -0
  62. package/packages/chat/scenarios.tsx +68 -0
  63. package/packages/chat/settings.tsx +98 -0
  64. package/packages/chat/temperature-slider.tsx +67 -0
  65. package/packages/chat/tool-results.tsx +32 -0
  66. package/packages/crud/core.ts +75 -0
  67. package/packages/crud/fetcher.ts +64 -0
  68. package/packages/crud/index.ts +2 -0
  69. package/packages/crud/next.ts +128 -0
  70. package/packages/crud/react.tsx +365 -0
  71. package/packages/env.ts +8 -0
  72. package/packages/fields.tsx +157 -0
  73. package/packages/firebase.ts +13 -0
  74. package/packages/firestore.ts +51 -0
  75. package/packages/form.tsx +66 -0
  76. package/packages/next.ts +64 -0
  77. package/packages/openrouter.ts +4 -0
  78. package/packages/react/context.tsx +21 -0
  79. package/packages/react/crud.tsx +372 -0
  80. package/packages/react/hooks.ts +90 -0
  81. package/packages/react/store.tsx +20 -0
  82. package/packages/replit-db.ts +219 -0
  83. package/packages/wretch.ts +22 -0
  84. package/packages/yaml.ts +163 -0
  85. package/pnpm-workspace.yaml +4 -0
  86. package/server/db.ts +15 -0
@@ -0,0 +1,118 @@
1
+ 'use client'
2
+ import { UpdateForm, useSingle, useTable } from '@/app/module'
3
+ import { ChatHeader, HeaderTitle, HeaderMenu } from '@/packages/chat'
4
+ import {
5
+ IconButton,
6
+ Menu,
7
+ Dialog,
8
+ Button,
9
+ Text,
10
+ Portal,
11
+ Spinner
12
+ } from '@chakra-ui/react'
13
+ import { RiSideBarLine } from 'react-icons/ri'
14
+ import { SettingsDialog } from './settings'
15
+ import { ScenariosDialog } from './scenarios'
16
+ import { useState, useCallback } from 'react'
17
+
18
+ export default function Header({ onOpenFeed } : { onOpenFeed: () => void }) {
19
+
20
+ const { single: chat } = useSingle('chats')
21
+ const { remove, create } = useTable('chats')
22
+ const [isDeleting, setIsDeleting] = useState(false)
23
+
24
+ const handleNewScenario = useCallback(async () => {
25
+ try {
26
+ const newChat = await create.trigger({
27
+ data: {
28
+ name: 'New Chat',
29
+ user: 'anonymous'
30
+ }
31
+ })
32
+ window.location.href = `/chat/${newChat.id}`
33
+ } catch (error) {
34
+ console.error('Error creating chat:', error)
35
+ }
36
+ }, [create])
37
+
38
+ const handleDeleteScenario = useCallback(async () => {
39
+ if (!chat?.id) return
40
+
41
+ setIsDeleting(true)
42
+ try {
43
+ await remove.trigger({ id: chat.id })
44
+ // Use window.location for immediate navigation after delete
45
+ window.location.href = '/chats'
46
+ } catch (error) {
47
+ console.error('Error deleting chat:', error)
48
+ setIsDeleting(false)
49
+ }
50
+ }, [chat?.id, remove])
51
+
52
+ return (
53
+ <ChatHeader>
54
+ <UpdateForm table='chats' id={chat.id} defaults={{name: chat.name}}>
55
+ {(props) =>
56
+ <HeaderTitle
57
+ value={props.fields.name}
58
+ defaultValue={chat.name}
59
+ onChange={(value) => props.callback({ name: value })} />
60
+ }
61
+ </UpdateForm>
62
+ <IconButton display={{base: 'block', md: 'none'}} onClick={onOpenFeed} variant='plain'>
63
+ <RiSideBarLine/>
64
+ </IconButton>
65
+ <HeaderMenu>
66
+ <SettingsDialog />
67
+ <Menu.Separator/>
68
+ <ScenariosDialog />
69
+ <Menu.Separator/>
70
+ <Menu.Item
71
+ value='new-scenario'
72
+ onClick={handleNewScenario}
73
+ disabled={create.loading}
74
+ >
75
+ New Scenario
76
+ <Menu.ItemCommand>
77
+ {create.loading ? <Spinner size='sm' /> : '➕'}
78
+ </Menu.ItemCommand>
79
+ </Menu.Item>
80
+ <Menu.Separator/>
81
+ <Dialog.Root>
82
+ <Dialog.Trigger asChild>
83
+ <Menu.Item value='delete-scenario' color='red.500'>Delete Scenario <Menu.ItemCommand>❌</Menu.ItemCommand></Menu.Item>
84
+ </Dialog.Trigger>
85
+ <Portal>
86
+ <Dialog.Positioner>
87
+ <Dialog.Content>
88
+ <Dialog.Header>
89
+ <Dialog.Title>Delete Scenario</Dialog.Title>
90
+ <Dialog.CloseTrigger />
91
+ </Dialog.Header>
92
+ <Dialog.Body>
93
+ <Text>Are you sure you want to delete "{chat?.name}"?</Text>
94
+ <Text fontSize='sm' color='gray.500' mt={2}>
95
+ This action cannot be undone. All messages in this chat will be permanently deleted.
96
+ </Text>
97
+ </Dialog.Body>
98
+ <Dialog.Footer>
99
+ <Dialog.ActionTrigger asChild>
100
+ <Button variant='ghost'>Cancel</Button>
101
+ </Dialog.ActionTrigger>
102
+ <Button
103
+ colorScheme='red'
104
+ onClick={handleDeleteScenario}
105
+ loading={isDeleting}
106
+ disabled={isDeleting}
107
+ >
108
+ Delete
109
+ </Button>
110
+ </Dialog.Footer>
111
+ </Dialog.Content>
112
+ </Dialog.Positioner>
113
+ </Portal>
114
+ </Dialog.Root>
115
+ </HeaderMenu>
116
+ </ChatHeader>
117
+ )
118
+ }
@@ -0,0 +1,14 @@
1
+ export * from './components'
2
+
3
+ // Chat data model components
4
+ export { default as Messages } from './messages'
5
+ export { ChatMessage } from './message'
6
+ export { EditMessage } from './edit-message'
7
+ export { MessageMenu } from './message-menu'
8
+ export { ToolResults, ToolResult } from './tool-results'
9
+ export { SettingsDialog } from './settings'
10
+ export { ScenariosDialog } from './scenarios'
11
+ export { ModelSelector } from './model-selector'
12
+ export { TemperatureSlider } from './temperature-slider'
13
+ export { default as Header } from './header'
14
+ export { default as Input } from './input'
@@ -0,0 +1,89 @@
1
+ 'use client'
2
+
3
+ import { CreateForm, useSingle, useTable } from '@/app/module'
4
+ import { ChatInput, InputMenu, InputTextarea, InputSend } from '@/packages/chat'
5
+ import { HStack, Menu } from '@chakra-ui/react'
6
+ import { CharactersDialog } from '@/app/chat/[id]/characters-dialog'
7
+
8
+ interface ChatInputProps {
9
+ generateResponse: (params: {
10
+ model?: string
11
+ system?: string
12
+ messages: any[]
13
+ chatId: string
14
+ temperature?: number
15
+ }) => Promise<any>
16
+ }
17
+
18
+ export default function Input({ generateResponse }: ChatInputProps) {
19
+ const { single: chat } = useSingle('chats')
20
+ const { array: allMessages, create: createMessage } = useTable('messages')
21
+
22
+ const handleUserMessageSuccess = async (userMessage: Message) => {
23
+ // Get all messages for this chat to send to AI
24
+ const chatMessages = allMessages
25
+ .filter(msg => msg.chat === chat.id)
26
+ .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
27
+
28
+ const response = await generateResponse({
29
+ model: chat.model || 'openrouter/auto',
30
+ temperature: chat.temperature || 1,
31
+ system: chat.system,
32
+ chatId: chat.id,
33
+ messages: [...chatMessages, userMessage]
34
+ })
35
+
36
+ // Add the response message to the messages table using user timezone
37
+ await createMessage.trigger({
38
+ data: {...response.messageData, timestamp: new Date().toISOString()}
39
+ })
40
+ }
41
+
42
+ return (
43
+ <ChatInput>
44
+ <InputMenu>
45
+ <CharactersDialog />
46
+ </InputMenu>
47
+ <CreateForm
48
+ table='messages'
49
+ defaults={{role: 'user', chat: chat.id}}
50
+ onSuccess={handleUserMessageSuccess}
51
+ >
52
+ {(props) => (
53
+ <HStack w='full' alignItems='flex-end' asChild>
54
+ <form onSubmit={async (e) => {
55
+ e.preventDefault()
56
+ if (!props.fields.content?.trim()) return
57
+
58
+ // Store current content to restore if submission fails
59
+ const currentContent = props.fields.content
60
+
61
+ try {
62
+ // Clear content before submission to show immediate feedback
63
+ props.setField('content', '')
64
+
65
+ // Submit the form using the callback method with timestamp
66
+ await props.callback({
67
+ ...props.fields,
68
+ content: currentContent,
69
+ timestamp: new Date().toISOString()
70
+ })
71
+ } catch (error) {
72
+ // Restore content if submission fails
73
+ props.setField('content', currentContent)
74
+ console.error('Failed to send message:', error)
75
+ // You could also show a toast notification here
76
+ }
77
+ }}>
78
+ <InputTextarea
79
+ value={props.fields.content}
80
+ onChange={e => props.setField('content', e.target.value)}
81
+ />
82
+ <InputSend loading={props.loading} type='submit' />
83
+ </form>
84
+ </HStack>
85
+ )}
86
+ </CreateForm>
87
+ </ChatInput>
88
+ )
89
+ }
@@ -0,0 +1,57 @@
1
+ import { useTable } from '@/app/module'
2
+ import { Prose } from '@/packages/chat'
3
+ import { Menu, Box } from '@chakra-ui/react'
4
+ import { LuPen, LuTrash2 } from 'react-icons/lu'
5
+ import ReactMarkdown from 'react-markdown'
6
+
7
+ export function MessageMenu({
8
+ message,
9
+ onOpen,
10
+ children
11
+ } : {
12
+ message: Message
13
+ onOpen: () => void
14
+ children: string
15
+ }) {
16
+ const { remove } = useTable('messages')
17
+
18
+ const handleDelete = () => {
19
+ if (!message) return
20
+ if (confirm('Are you sure you want to delete this message?')) {
21
+ remove.trigger({ id: message.id })
22
+ }
23
+ }
24
+
25
+ return (
26
+ <Menu.Root>
27
+ <Menu.Trigger asChild>
28
+ <Box cursor="pointer" position="relative" _hover={{ bg: 'gray.50' }} p={2} borderRadius="md">
29
+ <Prose fontSize='sm' textAlign='justify'>
30
+ <ReactMarkdown>{children}</ReactMarkdown>
31
+ </Prose>
32
+ </Box>
33
+ </Menu.Trigger>
34
+ <Menu.Content>
35
+ <Menu.Item
36
+ value="edit"
37
+ onClick={onOpen}
38
+ disabled={remove.loading}
39
+ >
40
+ <LuPen />
41
+ Edit message
42
+ </Menu.Item>
43
+ <Menu.Item
44
+ value="delete"
45
+ color="red.500"
46
+ disabled={remove.loading}
47
+ onClick={handleDelete}
48
+ >
49
+ <LuTrash2 />
50
+ {remove.loading ? 'Deleting...' : 'Delete message'}
51
+ </Menu.Item>
52
+ </Menu.Content>
53
+ </Menu.Root>
54
+ )
55
+ }
56
+
57
+ export default MessageMenu
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+ import { Box, Flex, useDisclosure } from '@chakra-ui/react'
3
+ import React from 'react'
4
+ import { EditMessage } from './edit-message'
5
+ import { MessageMenu } from './message-menu'
6
+ import { ToolResults } from './tool-results'
7
+
8
+ export const ChatMessage = ({ message }: { message: Message }) => {
9
+ const { open, onOpen, onClose } = useDisclosure()
10
+
11
+ if (!message) return null
12
+
13
+ const timestamp = new Date(message.timestamp).toLocaleTimeString()
14
+
15
+ if (open)
16
+ return <EditMessage message={message} onClose={onClose} />
17
+
18
+ return (
19
+ <Box position='relative'>
20
+ <Box fontSize='xs' w='full' mb={2}>
21
+ <Flex justifyContent='space-between'>
22
+ <Box><b>{message.role}</b></Box>
23
+ <Flex gap={2}>
24
+ <Box>{message.id}</Box>
25
+ <Box>{timestamp}</Box>
26
+ </Flex>
27
+ </Flex>
28
+ </Box>
29
+
30
+ <Box p={2} borderRadius='md'>
31
+ <MessageMenu message={message} onOpen={onOpen}>{message.content}</MessageMenu>
32
+ </Box>
33
+
34
+ {/* Render tool results if present */}
35
+ {message.tools && (
36
+ <Box mt={2} p={2} borderRadius='md' borderWidth='1px' borderColor='gray.100'>
37
+ <ToolResults tools={message.tools as Record<string, any>} />
38
+ </Box>
39
+ )}
40
+ </Box>
41
+ )
42
+ }
43
+
44
+ export default ChatMessage
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+ import { SingleProvider, UpdateForm, useSingle } from '@/app/module'
3
+ import { ChatMessages } from '@/packages/chat'
4
+ import { parseISO } from 'date-fns'
5
+ import { Editable } from '@chakra-ui/react'
6
+ import { useMemo } from 'react'
7
+ import { ChatMessage } from './message'
8
+
9
+ export default function Messages({
10
+ messages
11
+ } : {
12
+ messages: Message[]
13
+ }) {
14
+ const { single: chat } = useSingle('chats')
15
+ const sorted = useMemo(() => {
16
+ return [...messages].sort((a, b) => {
17
+ return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
18
+ }).reverse()
19
+ }, [messages])
20
+ return (
21
+ <ChatMessages>
22
+ {sorted.map(message => (
23
+ <ChatMessage key={message.id} message={message} />
24
+ ))}
25
+ <UpdateForm table='chats' id={chat.id} defaults={{system: chat.system}}>
26
+ {(props) => {
27
+ return (
28
+ <Editable.Root
29
+ mb='auto'
30
+ placeholder='System instructions'
31
+ value={props.fields.system}
32
+ defaultValue={chat.system}
33
+ onValueChange={({value}) => props.setField('system', value)}
34
+ onValueCommit={() => props.submit()}
35
+ >
36
+ <Editable.Preview/>
37
+ <Editable.Textarea/>
38
+ </Editable.Root>
39
+ )
40
+ }}
41
+ </UpdateForm>
42
+ </ChatMessages>
43
+ )
44
+ }
@@ -0,0 +1,172 @@
1
+ 'use client'
2
+ import { useState, useMemo } from 'react'
3
+ import { useAsync } from 'react-use'
4
+ import { VStack, Text, Alert, Listbox, HStack, Spinner, createListCollection, Box } from '@chakra-ui/react'
5
+ import { LuCheck } from 'react-icons/lu'
6
+ import type { OpenRouterModel } from '@/hooks/useOpenRouterModels'
7
+
8
+ // Async function to fetch all models once
9
+ async function fetchAllOpenRouterModels(): Promise<OpenRouterModel[]> {
10
+ try {
11
+ const response = await fetch('https://openrouter.ai/api/v1/models', {
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ })
16
+
17
+ if (!response.ok) {
18
+ throw new Error(`Failed to fetch models: ${response.statusText}`)
19
+ }
20
+
21
+ const data = await response.json()
22
+
23
+ // Filter and sort models for better UX
24
+ const filteredModels = data.data
25
+ ?.filter((model: any) => {
26
+ if (!model.id || !model.name) return false
27
+ if (model.id.includes(':free')) return false // Exclude free models
28
+ return true
29
+ })
30
+ ?.sort((a: any, b: any) => a.name.localeCompare(b.name)) || []
31
+
32
+ return filteredModels
33
+ } catch (err) {
34
+ console.error('Error fetching OpenRouter models:', err)
35
+
36
+ // Return fallback models on error
37
+ return [
38
+ { id: 'openrouter/auto', name: 'Auto (Recommended)' },
39
+ { id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' },
40
+ { id: 'openai/gpt-4o', name: 'GPT-4o' },
41
+ { id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini' },
42
+ { id: 'google/gemini-pro', name: 'Gemini Pro' },
43
+ { id: 'meta-llama/llama-3.1-8b-instruct', name: 'Llama 3.1 8B' },
44
+ ]
45
+ }
46
+ }
47
+
48
+ interface ModelSelectorProps {
49
+ value: string
50
+ onValueChange: (value: string) => void
51
+ }
52
+
53
+ export function ModelSelector({ value, onValueChange }: ModelSelectorProps) {
54
+ const [allModels, setAllModels] = useState<OpenRouterModel[]>([])
55
+
56
+ // Load all models once when component mounts
57
+ const asyncState = useAsync(async () => {
58
+ const models = await fetchAllOpenRouterModels()
59
+ setAllModels(models)
60
+ return models
61
+ }, [])
62
+
63
+ // Create list collection for Listbox
64
+ const collection = useMemo(() => {
65
+ return createListCollection({
66
+ items: allModels.map(model => ({
67
+ label: model.name,
68
+ value: model.id,
69
+ description: model.description,
70
+ context_length: model.context_length,
71
+ pricing: model.pricing
72
+ }))
73
+ })
74
+ }, [allModels])
75
+
76
+ // Find selected model name
77
+ const selectedModelName = useMemo(() => {
78
+ const selected = allModels.find(model => model.id === value)
79
+ return selected?.name || value || 'Auto (Recommended)'
80
+ }, [allModels, value])
81
+
82
+ return (
83
+ <VStack align="stretch" gap={2}>
84
+ <Text fontWeight="medium">Model</Text>
85
+ <Text fontSize="sm" color="gray.600">
86
+ {selectedModelName}
87
+ </Text>
88
+
89
+ {asyncState.error && (
90
+ <Alert.Root status="warning" size="sm">
91
+ <Alert.Indicator />
92
+ <Alert.Description>
93
+ Failed to load models. Using fallback options.
94
+ </Alert.Description>
95
+ </Alert.Root>
96
+ )}
97
+
98
+ {asyncState.loading ? (
99
+ <HStack justify="center" p={4}>
100
+ <Spinner size="sm" />
101
+ <Text>Loading models...</Text>
102
+ </HStack>
103
+ ) : (
104
+ <Box>
105
+ <Listbox.Root
106
+ collection={collection}
107
+ value={[value || 'openrouter/auto']}
108
+ onValueChange={({ value }) => {
109
+ if (value[0]) {
110
+ onValueChange(value[0])
111
+ }
112
+ }}
113
+ >
114
+ <Listbox.Label fontSize="sm" fontWeight="medium" mb={2}>
115
+ Select AI Model
116
+ </Listbox.Label>
117
+ <Listbox.Content
118
+ maxH="300px"
119
+ overflowY="auto"
120
+ overflowX="hidden"
121
+ borderWidth="1px"
122
+ borderRadius="md"
123
+ p={1}
124
+ >
125
+ {collection.items.map((model) => (
126
+ <Listbox.Item
127
+ key={model.value}
128
+ item={model}
129
+ p={3}
130
+ borderRadius="sm"
131
+ _hover={{ bg: "gray.50" }}
132
+ _selected={{ bg: "blue.50", borderColor: "blue.300", borderWidth: "1px" }}
133
+ >
134
+ <HStack justify="space-between" align="start">
135
+ <VStack align="start" gap={1} flex={1}>
136
+ <Text fontWeight="medium" fontSize="sm">
137
+ {model.label}
138
+ </Text>
139
+ {model.description && (
140
+ <Text fontSize="xs" color="gray.600">
141
+ {model.description}
142
+ </Text>
143
+ )}
144
+ {model.context_length && (
145
+ <Text fontSize="xs" color="gray.500">
146
+ Context: {model.context_length.toLocaleString()} tokens
147
+ </Text>
148
+ )}
149
+ {model.pricing && (
150
+ <HStack fontSize="xs" color="gray.500" gap={3}>
151
+ {model.pricing.prompt && (
152
+ <Text>Input: ${model.pricing.prompt}/1M tokens</Text>
153
+ )}
154
+ {model.pricing.completion && (
155
+ <Text>Output: ${model.pricing.completion}/1M tokens</Text>
156
+ )}
157
+ </HStack>
158
+ )}
159
+ </VStack>
160
+ <Listbox.ItemIndicator>
161
+ <LuCheck />
162
+ </Listbox.ItemIndicator>
163
+ </HStack>
164
+ </Listbox.Item>
165
+ ))}
166
+ </Listbox.Content>
167
+ </Listbox.Root>
168
+ </Box>
169
+ )}
170
+ </VStack>
171
+ )
172
+ }
@@ -0,0 +1,68 @@
1
+ 'use client'
2
+
3
+ import React, { useState } from 'react'
4
+ import {
5
+ Button,
6
+ Dialog,
7
+ Menu,
8
+ VStack,
9
+ HStack,
10
+ Portal} from '@chakra-ui/react'
11
+ import { LuCheck } from 'react-icons/lu'
12
+ import { useRouter } from 'next/navigation'
13
+ import { CreateChatButton } from '@/app/chats/create-chat-button'
14
+ import { ChatsList } from '@/app/chats/chats-list'
15
+
16
+ export interface CharactersDialogProps {
17
+ onClose?: () => void
18
+ }
19
+
20
+ interface ScenariosDialogProps {
21
+ onClose?: () => void
22
+ }
23
+
24
+ export function ScenariosDialog({ onClose }: ScenariosDialogProps) {
25
+ const router = useRouter()
26
+ const [open, setOpen] = useState(false)
27
+
28
+ const handleChatSelect = (chatId: string) => {
29
+ router.push(`/chat/${chatId}`)
30
+ setOpen(false)
31
+ onClose?.()
32
+ }
33
+
34
+ return (
35
+ <Dialog.Root open={open} onOpenChange={(e) => setOpen(e.open)}>
36
+ <Dialog.Trigger asChild>
37
+ <Menu.Item value='scenarios'>My Scenarios <Menu.ItemCommand>🎭</Menu.ItemCommand></Menu.Item>
38
+ </Dialog.Trigger>
39
+ <Portal>
40
+ <Dialog.Positioner>
41
+ <Dialog.Backdrop/>
42
+ <Dialog.Content maxW='container.sm'>
43
+ <Dialog.Header>
44
+ <Dialog.Title>My Scenarios</Dialog.Title>
45
+ <Dialog.CloseTrigger />
46
+ </Dialog.Header>
47
+ <Dialog.Body>
48
+ <VStack gap={4} align='stretch'>
49
+ <HStack justify='end'>
50
+ <CreateChatButton />
51
+ </HStack>
52
+ <ChatsList
53
+ onChatSelect={handleChatSelect}
54
+ showDeleteButton={true}
55
+ />
56
+ </VStack>
57
+ </Dialog.Body>
58
+ <Dialog.Footer>
59
+ <Dialog.ActionTrigger asChild>
60
+ <Button variant='ghost'>Close</Button>
61
+ </Dialog.ActionTrigger>
62
+ </Dialog.Footer>
63
+ </Dialog.Content>
64
+ </Dialog.Positioner>
65
+ </Portal>
66
+ </Dialog.Root>
67
+ )
68
+ }