Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions apps/openfaith/app/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Route as MarketingFeaturesRouteImport } from './routes/_marketing/featu
import { Route as MarketingBlogRouteImport } from './routes/_marketing/blog'
import { Route as AuthSignInRouteImport } from './routes/_auth/sign-in'
import { Route as AppDashboardRouteImport } from './routes/_app/dashboard'
import { Route as AppAiRouteImport } from './routes/_app/ai'
import { Route as AppGroupRouteRouteImport } from './routes/_app/$group/route'
import { Route as AppSettingsTeamRouteImport } from './routes/_app/settings/team'
import { Route as AppSettingsProfileRouteImport } from './routes/_app/settings/profile'
Expand All @@ -39,6 +40,7 @@ import { Route as AppGroupEntityEntityIdRouteImport } from './routes/_app/$group
import { ServerRoute as ApiSplatServerRouteImport } from './routes/api/$'
import { ServerRoute as ApiAuthRefreshServerRouteImport } from './routes/api/auth/refresh'
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth/$'
import { ServerRoute as ApiAiChatServerRouteImport } from './routes/api/ai/chat'

const rootServerRouteImport = createServerRootRoute()

Expand Down Expand Up @@ -108,6 +110,11 @@ const AppDashboardRoute = AppDashboardRouteImport.update({
path: '/dashboard',
getParentRoute: () => AppRouteRoute,
} as any)
const AppAiRoute = AppAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => AppRouteRoute,
} as any)
const AppGroupRouteRoute = AppGroupRouteRouteImport.update({
id: '/$group',
path: '/$group',
Expand Down Expand Up @@ -178,9 +185,15 @@ const ApiAuthSplatServerRoute = ApiAuthSplatServerRouteImport.update({
path: '/api/auth/$',
getParentRoute: () => rootServerRouteImport,
} as any)
const ApiAiChatServerRoute = ApiAiChatServerRouteImport.update({
id: '/api/ai/chat',
path: '/api/ai/chat',
getParentRoute: () => rootServerRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/$group': typeof AppGroupRouteRouteWithChildren
'/ai': typeof AppAiRoute
'/dashboard': typeof AppDashboardRoute
'/sign-in': typeof AuthSignInRoute
'/blog': typeof MarketingBlogRoute
Expand All @@ -204,6 +217,7 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/$group': typeof AppGroupRouteRouteWithChildren
'/ai': typeof AppAiRoute
'/dashboard': typeof AppDashboardRoute
'/sign-in': typeof AuthSignInRoute
'/blog': typeof MarketingBlogRoute
Expand Down Expand Up @@ -231,6 +245,7 @@ export interface FileRoutesById {
'/_marketing': typeof MarketingRouteRouteWithChildren
'/_onboarding': typeof OnboardingRouteRouteWithChildren
'/_app/$group': typeof AppGroupRouteRouteWithChildren
'/_app/ai': typeof AppAiRoute
'/_app/dashboard': typeof AppDashboardRoute
'/_auth/sign-in': typeof AuthSignInRoute
'/_marketing/blog': typeof MarketingBlogRoute
Expand All @@ -256,6 +271,7 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/$group'
| '/ai'
| '/dashboard'
| '/sign-in'
| '/blog'
Expand All @@ -279,6 +295,7 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/$group'
| '/ai'
| '/dashboard'
| '/sign-in'
| '/blog'
Expand All @@ -305,6 +322,7 @@ export interface FileRouteTypes {
| '/_marketing'
| '/_onboarding'
| '/_app/$group'
| '/_app/ai'
| '/_app/dashboard'
| '/_auth/sign-in'
| '/_marketing/blog'
Expand Down Expand Up @@ -336,30 +354,39 @@ export interface RootRouteChildren {
}
export interface FileServerRoutesByFullPath {
'/api/$': typeof ApiSplatServerRoute
'/api/ai/chat': typeof ApiAiChatServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/auth/refresh': typeof ApiAuthRefreshServerRoute
}
export interface FileServerRoutesByTo {
'/api/$': typeof ApiSplatServerRoute
'/api/ai/chat': typeof ApiAiChatServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/auth/refresh': typeof ApiAuthRefreshServerRoute
}
export interface FileServerRoutesById {
__root__: typeof rootServerRouteImport
'/api/$': typeof ApiSplatServerRoute
'/api/ai/chat': typeof ApiAiChatServerRoute
'/api/auth/$': typeof ApiAuthSplatServerRoute
'/api/auth/refresh': typeof ApiAuthRefreshServerRoute
}
export interface FileServerRouteTypes {
fileServerRoutesByFullPath: FileServerRoutesByFullPath
fullPaths: '/api/$' | '/api/auth/$' | '/api/auth/refresh'
fullPaths: '/api/$' | '/api/ai/chat' | '/api/auth/$' | '/api/auth/refresh'
fileServerRoutesByTo: FileServerRoutesByTo
to: '/api/$' | '/api/auth/$' | '/api/auth/refresh'
id: '__root__' | '/api/$' | '/api/auth/$' | '/api/auth/refresh'
to: '/api/$' | '/api/ai/chat' | '/api/auth/$' | '/api/auth/refresh'
id:
| '__root__'
| '/api/$'
| '/api/ai/chat'
| '/api/auth/$'
| '/api/auth/refresh'
fileServerRoutesById: FileServerRoutesById
}
export interface RootServerRouteChildren {
ApiSplatServerRoute: typeof ApiSplatServerRoute
ApiAiChatServerRoute: typeof ApiAiChatServerRoute
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
ApiAuthRefreshServerRoute: typeof ApiAuthRefreshServerRoute
}
Expand Down Expand Up @@ -464,6 +491,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppDashboardRouteImport
parentRoute: typeof AppRouteRoute
}
'/_app/ai': {
id: '/_app/ai'
path: '/ai'
fullPath: '/ai'
preLoaderRoute: typeof AppAiRouteImport
parentRoute: typeof AppRouteRoute
}
'/_app/$group': {
id: '/_app/$group'
path: '/$group'
Expand Down Expand Up @@ -566,6 +600,13 @@ declare module '@tanstack/react-start/server' {
preLoaderRoute: typeof ApiAuthSplatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
'/api/ai/chat': {
id: '/api/ai/chat'
path: '/api/ai/chat'
fullPath: '/api/ai/chat'
preLoaderRoute: typeof ApiAiChatServerRouteImport
parentRoute: typeof rootServerRouteImport
}
}
}

Expand Down Expand Up @@ -596,6 +637,7 @@ const AppGroupRouteRouteWithChildren = AppGroupRouteRoute._addFileChildren(

interface AppRouteRouteChildren {
AppGroupRouteRoute: typeof AppGroupRouteRouteWithChildren
AppAiRoute: typeof AppAiRoute
AppDashboardRoute: typeof AppDashboardRoute
AppAdminOrgsRoute: typeof AppAdminOrgsRoute
AppAdminUsersRoute: typeof AppAdminUsersRoute
Expand All @@ -608,6 +650,7 @@ interface AppRouteRouteChildren {

const AppRouteRouteChildren: AppRouteRouteChildren = {
AppGroupRouteRoute: AppGroupRouteRouteWithChildren,
AppAiRoute: AppAiRoute,
AppDashboardRoute: AppDashboardRoute,
AppAdminOrgsRoute: AppAdminOrgsRoute,
AppAdminUsersRoute: AppAdminUsersRoute,
Expand Down Expand Up @@ -680,6 +723,7 @@ export const routeTree = rootRouteImport
._addFileTypes<FileRouteTypes>()
const rootServerRouteChildren: RootServerRouteChildren = {
ApiSplatServerRoute: ApiSplatServerRoute,
ApiAiChatServerRoute: ApiAiChatServerRoute,
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
ApiAuthRefreshServerRoute: ApiAuthRefreshServerRoute,
}
Expand Down
190 changes: 190 additions & 0 deletions apps/openfaith/app/routes/_app/ai.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useChat } from '@ai-sdk/react'
import { Results } from '@openfaith/openfaith/features/ai/results'
import type { Config, Result } from '@openfaith/openfaith/features/ai/tools'
import {
Action,
Actions,
Conversation,
ConversationContent,
ConversationScrollButton,
CopyIcon,
Loader,
Message,
MessageContent,
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputFooter,
type PromptInputMessage,
PromptInputSubmit,
PromptInputTextarea,
PromptInputTools,
Reasoning,
ReasoningContent,
ReasoningTrigger,
RefreshIcon,
Response,
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from '@openfaith/ui'
import { createFileRoute } from '@tanstack/react-router'
import { DefaultChatTransport } from 'ai'
import { Array, pipe } from 'effect'
import { Fragment, useState } from 'react'

export const Route = createFileRoute('/_app/ai')({
component: RouteComponent,
})

function RouteComponent() {
const [input, setInput] = useState('')
const { messages, sendMessage, status, regenerate } = useChat({
transport: new DefaultChatTransport({
api: '/api/ai/chat',
}),
})

const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text)
const hasAttachments = Boolean(message.files?.length)

if (!(hasText || hasAttachments)) {
return
}

sendMessage({
text: message.text || 'Sent with attachments',
})
setInput('')
}

return (
<div className='relative mx-auto size-full h-screen max-w-4xl p-6'>
<div className='flex h-full flex-col'>
<Conversation className='h-full'>
<ConversationContent>
{pipe(
messages,
Array.map((message) => (
<div key={message.id}>
{message.role === 'assistant' &&
pipe(
message.parts,
Array.filter((part) => part.type === 'source-url'),
Array.length,
) > 0 && (
<Sources>
<SourcesTrigger
count={message.parts.filter((part) => part.type === 'source-url').length}
/>
{pipe(
message.parts,
Array.filter((part) => part.type === 'source-url'),
Array.map((part, i) => (
<SourcesContent key={`${message.id}-${i}`}>
<Source href={part.url} key={`${message.id}-${i}`} title={part.url} />
</SourcesContent>
)),
)}
</Sources>
)}
{pipe(
message.parts,
Array.map((part, i) => {
switch (part.type) {
case 'text':
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role}>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === 'assistant' && i === messages.length - 1 && (
<Actions className='mt-2'>
<Action label='Retry' onClick={() => regenerate()}>
<RefreshIcon className='size-3' />
</Action>
<Action
label='Copy'
onClick={() => navigator.clipboard.writeText(part.text)}
>
<CopyIcon className='size-3' />
</Action>
</Actions>
)}
</Fragment>
)
case 'tool-generateChartConfig': {
const output = part.output as { config?: Config } | undefined
const input = part.input as { results?: Array<Result> } | undefined

if (output && output.config && input && input.results) {
return (
<Message from='assistant' key={part.toolCallId}>
<MessageContent className='w-full'>
<Results chartConfig={output.config} results={input.results} />
</MessageContent>
</Message>
)
}
return null
}
case 'reasoning':
return (
<Reasoning
className='w-full'
isStreaming={
status === 'streaming' &&
i === message.parts.length - 1 &&
message.id === messages.at(-1)?.id
}
key={`${message.id}-${i}`}
>
<ReasoningTrigger />
<ReasoningContent>{part.text}</ReasoningContent>
</Reasoning>
)
default:
return null
}
}),
)}
</div>
)),
)}
{status === 'submitted' && <Loader />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>

<PromptInput className='mt-4' globalDrop multiple onSubmit={handleSubmit}>
<PromptInputBody>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
<PromptInputTextarea onChange={(e) => setInput(e.target.value)} value={input} />
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputSubmit disabled={!input && !status} status={status} />
</PromptInputFooter>
</PromptInput>
</div>
</div>
)
}
Loading