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
|
@@ -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
|
+
}
|