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
108 changes: 83 additions & 25 deletions components/chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ import { Stream, Message } from "@/lib/OpenRouter"
import { MessageView } from "./Messages"
import { useDB } from "@/components/DatabaseProvider"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { openUrl } from '@tauri-apps/plugin-opener'

import SourceDropdown from "./SourceDropdown"
import Settings from "./Settings"
import OpenAI from "openai"

import {
WandSparklesIcon,
SendHorizontalIcon,
GlobeIcon,
WrenchIcon,
CheckIcon
CheckIcon,
KeyRoundIcon,
} from "lucide-react"

import {
Expand All @@ -26,6 +31,33 @@ import {

type FormEvent = React.KeyboardEvent<HTMLInputElement>

// SetUpKeyCard is a card that asks the user to set up their OpenRouter
// API key whenever it's not available.
export const SetUpKeyCard = () => {
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
const openrouter = () => {
openUrl("https://openrouter.ai/")
}
return (
<div className="pt-12 min-h-full flex flex-col justify-center items-center pb-12">
<KeyRoundIcon className="h-12 w-12"/>
<p className="text-lg font-bold pt-2 text-center">
Configure API Key
</p>
<p className="text-center pb-4">
Configure your OpenRouter API key to begin sending messages.
</p>
<div className="w-100 flex flex-col gap-2">
<Button onClick={openrouter}> Get Free API Key (OpenRouter) </Button>
<Button onClick={() => setSettingsOpen(true)}>
Configure API Key
</Button>
</div>
<Settings open={settingsOpen} onOpenChange={setSettingsOpen} />
</div>
)
}

export default function Chat() {
const [messages, setMessages] = useState<(Message|ToolCall)[]>([])
const [source, setSource] = useState<string>('google/gemini-2.5-pro-exp-03-25:free')
Expand All @@ -34,6 +66,8 @@ export default function Chat() {
const [isStreaming, setIsStreaming] = useState<boolean>(false)
const [webSearchEnabled, setWebSearchEnabled] = useState<boolean>(false)
const [toolCallingEnabled, setToolCallingEnabled] = useState<boolean>(true)
const [hasKey, setHasKey] = useState<boolean>(false)
const [isLoading, setIsLoading] = useState<boolean>(true)
const [client, setClient] = useState<OpenAI>(new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
dangerouslyAllowBrowser: true,
Expand All @@ -44,36 +78,51 @@ export default function Chat() {

// Fetch messages and API key from database.
useEffect(() => {
// TODO: HANDLE DATABASE ERRORS.
const fetchMessages = async () => {
const msgs = await db.history.readAll()
setMessages(msgs.map(msg => {
if (msg.role === 'tool') {
try {
const msgs = await db.history.readAll()
setMessages(msgs.map(msg => {
if (msg.role === 'tool') {
return {
tool : msg.tool_name,
id : msg.id,
content : msg.content,
} as ToolCall
}
return {
tool : msg.tool_name,
id : msg.id,
content : msg.content,
} as ToolCall
}
return {
role : msg.role,
content : msg.content
} as Message
}))
setIsTyping(false)
role : msg.role,
content : msg.content
} as Message
}))
}
catch {
toast('Error: Failed to Load Chat Message', {
description: 'The database failed to load chat message'
})
}
finally {setIsTyping(false)}
}
const fetchKey = async () => {
const keys = await db.keys.readAll()
if (keys.length === 0) {
// TODO: SHOW DIALOG
return
try {
const keys = await db.keys.readAll()
if (keys.length === 0) {
setHasKey(false)
return
}
setClient(new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
dangerouslyAllowBrowser: true,
apiKey: keys[0].key_hash,
}))
setHasKey(true)
}
catch {
toast('Error: Failed to fetch API Key', {
description: 'Ensure your API Key is correct'
})
}
setClient(new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
dangerouslyAllowBrowser: true,
apiKey: keys[0].key_hash,
}))
}
setIsLoading(false)
fetchMessages()
fetchKey()
}, [])
Expand Down Expand Up @@ -167,6 +216,15 @@ export default function Chat() {
}
}

if (isLoading) {
setIsLoading(false)
}

// Show a "set up API key" card when OpenRouter key is not configured.
if (!hasKey) {
return <SetUpKeyCard />
}

return (
<div className="pt-12 min-h-full grid grid-rows-[auto_40px]">
<div className="absolute top-2">
Expand Down
27 changes: 14 additions & 13 deletions components/chat/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
'use client'

import { useState, useEffect } from "react"
import { useState, useEffect, Dispatch, SetStateAction } from "react"
import { Button } from "@/components/ui/button"
import { SettingsIcon } from "lucide-react"
import { useDB } from "@/components/DatabaseProvider"
import { toast } from "sonner"
import { Keys } from "@/lib/controller/KeyController"

import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"

type SettingsProps = {
onOpenChange: Dispatch<SetStateAction<boolean>>
open: boolean,
}

// Settings displays a modal asking the user to configure their
// OpenRouter API key.
const Settings = () => {
const [open, setOpen] = useState(false)
const Settings = ({open, onOpenChange} : SettingsProps) => {
const [apiKey, setApiKey] = useState("")
const db = useDB()

Expand All @@ -35,7 +37,7 @@ const Settings = () => {
setApiKey("")
return
}
setApiKey(keys[0].id!.toString())
setApiKey(keys[0].key_hash)
}
fetchKey()
}, [])
Expand All @@ -45,6 +47,10 @@ const Settings = () => {
const handleSubmit = async () => {
try {
await db.keys.deleteAll()
if (apiKey === "") {
onOpenChange(false)
return
}
await db.keys.create({
key_hash : apiKey,
created_at : Math.floor(Date.now() / 1000)
Expand All @@ -54,16 +60,11 @@ const Settings = () => {
description: 'A database error prevented the key from being saved'
})
}
setOpen(false)
onOpenChange(false)
}

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size='icon'>
<SettingsIcon />
</Button>
</DialogTrigger>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full !p-4">
<DialogHeader className="col-span-2">
<DialogTitle>Enter your OpenRouter API Key</DialogTitle>
Expand Down
42 changes: 25 additions & 17 deletions components/navigation/RightNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,35 @@
import { Button } from "@/components/ui/button"
import { NavigationState } from "./NavigationState"
import { MessageSquareIcon, PlusIcon} from "lucide-react"
import { MessageSquareIcon, PlusIcon, SettingsIcon} from "lucide-react"
import { useState } from "react"

import Chat from "@/components/chat/Chat"
import Settings from "../chat/Settings"

// RightNavigation consists of the create note, LLM chat, and
// settings buttons.
const RightNavigation = ({ state } : { state: NavigationState }) => (
<div className='flex flex-row gap-1 fixed top-2 right-2 z-20'>
{ /* Redirecting to the note page creates a new note */ }
<Button size='icon' onClick={() => window.location.href = '/note'}>
<PlusIcon />
</Button>
{ /* Toggle the right sidebar */ }
<Button
onClick={() => state.setRightOpen(!state.isRightOpen)}
size='icon'
>
<MessageSquareIcon />
</Button>
<Settings />
</div>
)
const RightNavigation = ({ state } : { state: NavigationState }) => {
const [settingsOpen, setSettingsOpen] = useState<boolean>(false)
return (
<div className='flex flex-row gap-1 fixed top-2 right-2 z-20'>
{ /* Redirecting to the note page creates a new note */ }
<Button size='icon' onClick={() => window.location.href = '/note'}>
<PlusIcon />
</Button>
{ /* Toggle the right sidebar */ }
<Button
onClick={() => state.setRightOpen(!state.isRightOpen)}
size='icon'
>
<MessageSquareIcon />
</Button>
<Button size='icon' onClick={() => setSettingsOpen(true)}>
<SettingsIcon />
</Button>
<Settings open={settingsOpen} onOpenChange={setSettingsOpen} />
</div>
)
}

// RightSidebar shows the LLM chat sidebar.
const RightSidebar = () => (
Expand Down