styling fixes (#24)

This commit is contained in:
Aidan Bleser 2025-06-18 13:16:16 -05:00 committed by GitHub
parent df66ccbb2a
commit fd9407d681
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 179 additions and 80 deletions

View file

@ -364,12 +364,13 @@ export const search = query({
// If we have matches in title or messages, add to results // If we have matches in title or messages, add to results
if (titleResults.length > 0 || messageResults.length > 0) { if (titleResults.length > 0 || messageResults.length > 0) {
const titleScore = titleResults.length > 0 ? titleResults[0]?.score ?? 0 : 0; const titleScore = titleResults.length > 0 ? (titleResults[0]?.score ?? 0) : 0;
const messageScore = messageResults.length > 0 ? Math.max(...messageResults.map(r => r.score)) : 0; const messageScore =
messageResults.length > 0 ? Math.max(...messageResults.map((r) => r.score)) : 0;
results.push({ results.push({
conversation, conversation,
messages: messageResults.map(r => r.item), messages: messageResults.map((r) => r.item),
score: Math.max(titleScore, messageScore), score: Math.max(titleScore, messageScore),
titleMatch: titleResults.length > 0, titleMatch: titleResults.length > 0,
}); });

View file

@ -62,6 +62,9 @@ export const create = mutation({
throw new Error('Unauthorized'); throw new Error('Unauthorized');
} }
const conversation = await ctx.db.get(args.conversation_id as Id<'conversations'>);
if (conversation?.user_id !== session.userId) throw new Error('Unauthorized');
// I think this just slows us down // I think this just slows us down
// const messages = await ctx.runQuery(api.messages.getAllFromConversation, { // const messages = await ctx.runQuery(api.messages.getAllFromConversation, {

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Sidebar, useSidebarControls } from '$lib/components/ui/sidebar'; import * as Sidebar from '$lib/components/ui/sidebar';
import { useSidebarControls } from '$lib/components/ui/sidebar';
import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte'; import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte';
import Tooltip from '$lib/components/ui/tooltip.svelte'; import Tooltip from '$lib/components/ui/tooltip.svelte';
import { cn } from '$lib/utils/utils.js'; import { cn } from '$lib/utils/utils.js';
@ -17,6 +18,10 @@
import XIcon from '~icons/lucide/x'; import XIcon from '~icons/lucide/x';
import { page } from '$app/state'; import { page } from '$app/state';
import { Button } from './ui/button'; import { Button } from './ui/button';
import SearchIcon from '~icons/lucide/search';
import PanelLeftIcon from '~icons/lucide/panel-left';
let { searchModalOpen = $bindable(false) }: { searchModalOpen: boolean } = $props();
const client = useConvexClient(); const client = useConvexClient();
@ -115,13 +120,28 @@
{ key: 'lastMonth', label: 'Last 30 days', conversations: groupedConversations.lastMonth }, { key: 'lastMonth', label: 'Last 30 days', conversations: groupedConversations.lastMonth },
{ key: 'older', label: 'Older', conversations: groupedConversations.older }, { key: 'older', label: 'Older', conversations: groupedConversations.older },
]); ]);
function openSearchModal() {
searchModalOpen = true;
}
</script> </script>
<Sidebar class="flex flex-col overflow-clip p-2"> <Sidebar.Sidebar class="flex flex-col overflow-clip p-2">
<div class="flex place-items-center justify-center py-2"> <div class="flex place-items-center justify-between py-2">
<div>
<Tooltip>
{#snippet trigger(tooltip)}
<Sidebar.Trigger {...tooltip.trigger}>
<PanelLeftIcon />
</Sidebar.Trigger>
{/snippet}
Toggle Sidebar ({cmdOrCtrl} + B)
</Tooltip>
</div>
<span class="text-center font-serif text-lg">Thom.chat</span> <span class="text-center font-serif text-lg">Thom.chat</span>
<div class="size-9"></div>
</div> </div>
<div class="mt-1 flex w-full px-2"> <div class="mt-1 flex w-full flex-col gap-2 px-2">
<Tooltip> <Tooltip>
{#snippet trigger(tooltip)} {#snippet trigger(tooltip)}
<a <a
@ -134,7 +154,21 @@
New Chat New Chat
</a> </a>
{/snippet} {/snippet}
{cmdOrCtrl} + Shift + O New Chat ({cmdOrCtrl} + Shift + O)
</Tooltip>
<Tooltip>
{#snippet trigger(tooltip)}
<button
{...tooltip.trigger}
type="button"
class="text-muted-foreground font-fake-proxima border-border flex place-items-center gap-2 border-b py-2 md:text-sm"
onclick={openSearchModal}
>
<SearchIcon class="size-4" />
<span class="text-muted-foreground/50">Search conversations...</span>
</button>
{/snippet}
Search ({cmdOrCtrl} + K)
</Tooltip> </Tooltip>
</div> </div>
<div class="relative flex min-h-0 flex-1 shrink-0 flex-col overflow-clip"> <div class="relative flex min-h-0 flex-1 shrink-0 flex-col overflow-clip">
@ -268,4 +302,4 @@
<Button href="/login" class="w-full">Login</Button> <Button href="/login" class="w-full">Login</Button>
{/if} {/if}
</div> </div>
</Sidebar> </Sidebar.Sidebar>

View file

@ -4,9 +4,17 @@
import { useSidebar } from './sidebar.svelte.js'; import { useSidebar } from './sidebar.svelte.js';
import { shortcut } from '$lib/actions/shortcut.svelte.js'; import { shortcut } from '$lib/actions/shortcut.svelte.js';
let { children, ...rest }: HTMLAttributes<HTMLDivElement> = $props(); let {
open = $bindable(false),
children,
...rest
}: HTMLAttributes<HTMLDivElement> & { open?: boolean } = $props();
const sidebar = useSidebar(); const sidebar = useSidebar();
$effect(() => {
open = sidebar.showSidebar;
});
</script> </script>
<svelte:window use:shortcut={{ key: 'b', ctrl: true, callback: sidebar.toggle }} /> <svelte:window use:shortcut={{ key: 'b', ctrl: true, callback: sidebar.toggle }} />

View file

@ -44,11 +44,16 @@ export class SidebarSidebarState {
export class SidebarControlState { export class SidebarControlState {
constructor(readonly root: SidebarRootState) { constructor(readonly root: SidebarRootState) {
this.closeMobile = this.closeMobile.bind(this); this.closeMobile = this.closeMobile.bind(this);
this.toggle = this.toggle.bind(this);
} }
closeMobile() { closeMobile() {
this.root.closeMobile(); this.root.closeMobile();
} }
toggle() {
this.root.toggle();
}
} }
export const ctx = new Context<SidebarRootState>('sidebar-root-context'); export const ctx = new Context<SidebarRootState>('sidebar-root-context');

View file

@ -3,15 +3,25 @@ import type { GenerateMessageRequestBody, GenerateMessageResponse } from './+ser
export async function callGenerateMessage(args: GenerateMessageRequestBody) { export async function callGenerateMessage(args: GenerateMessageRequestBody) {
const res = ResultAsync.fromPromise( const res = ResultAsync.fromPromise(
fetch('/api/generate-message', { (async () => {
method: 'POST', const res = await fetch('/api/generate-message', {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
body: JSON.stringify(args), },
}), body: JSON.stringify(args),
(e) => e });
).map((r) => r.json() as Promise<GenerateMessageResponse>);
if (!res.ok) {
const { message } = await res.json();
throw new Error(message as string);
}
return res.json() as Promise<GenerateMessageResponse>;
})(),
(e) => `${e}`
);
return res; return res;
} }

View file

@ -32,25 +32,25 @@
import CheckIcon from '~icons/lucide/check'; import CheckIcon from '~icons/lucide/check';
import ChevronDownIcon from '~icons/lucide/chevron-down'; import ChevronDownIcon from '~icons/lucide/chevron-down';
import ImageIcon from '~icons/lucide/image'; import ImageIcon from '~icons/lucide/image';
import LockIcon from '~icons/lucide/lock';
import LockOpenIcon from '~icons/lucide/lock-open';
import PanelLeftIcon from '~icons/lucide/panel-left'; import PanelLeftIcon from '~icons/lucide/panel-left';
import SearchIcon from '~icons/lucide/search';
import Settings2Icon from '~icons/lucide/settings-2'; import Settings2Icon from '~icons/lucide/settings-2';
import ShareIcon from '~icons/lucide/share';
import StopIcon from '~icons/lucide/square';
import UploadIcon from '~icons/lucide/upload'; import UploadIcon from '~icons/lucide/upload';
import XIcon from '~icons/lucide/x'; import XIcon from '~icons/lucide/x';
import { callCancelGeneration } from '../api/cancel-generation/call.js'; import SearchIcon from '~icons/lucide/search';
import { callGenerateMessage } from '../api/generate-message/call.js'; import { callGenerateMessage } from '../api/generate-message/call.js';
import { callCancelGeneration } from '../api/cancel-generation/call.js';
import ModelPicker from './model-picker.svelte'; import ModelPicker from './model-picker.svelte';
import ShareIcon from '~icons/lucide/share';
import { fade } from 'svelte/transition';
import LockIcon from '~icons/lucide/lock';
import LockOpenIcon from '~icons/lucide/lock-open';
import StopIcon from '~icons/lucide/square';
import SearchModal from './search-modal.svelte'; import SearchModal from './search-modal.svelte';
const client = useConvexClient(); const client = useConvexClient();
let { children } = $props(); let { children } = $props();
let form = $state<HTMLFormElement>();
let textarea = $state<HTMLTextAreaElement>(); let textarea = $state<HTMLTextAreaElement>();
let abortController = $state<AbortController | null>(null); let abortController = $state<AbortController | null>(null);
@ -89,11 +89,20 @@
let loading = $state(false); let loading = $state(false);
const textareaDisabled = $derived(isGenerating || loading); const textareaDisabled = $derived(
isGenerating ||
loading ||
(currentConversationQuery.data &&
currentConversationQuery.data.user_id !== session.current?.user.id)
);
let error = $state<string | null>(null);
async function handleSubmit() { async function handleSubmit() {
if (isGenerating) return; if (isGenerating) return;
error = null;
// TODO: Re-use zod here from server endpoint for better error messages? // TODO: Re-use zod here from server endpoint for better error messages?
if (message.current === '' || !session.current?.user.id || !settings.modelId) return; if (message.current === '' || !session.current?.user.id || !settings.modelId) return;
@ -113,7 +122,8 @@
}); });
if (res.isErr()) { if (res.isErr()) {
return; // TODO: Handle error error = res._unsafeUnwrapErr() ?? 'An unknown error occurred';
return;
} }
const cid = res.value.conversation_id; const cid = res.value.conversation_id;
@ -384,6 +394,14 @@
() => !scrollState.arrived.bottom, () => !scrollState.arrived.bottom,
() => (mounted.current ? 250 : 0) () => (mounted.current ? 250 : 0)
); );
let searchModalOpen = $state(false);
function openSearchModal() {
searchModalOpen = true;
}
let sidebarOpen = $state(false);
</script> </script>
<svelte:head> <svelte:head>
@ -391,41 +409,58 @@
</svelte:head> </svelte:head>
<Sidebar.Root <Sidebar.Root
bind:open={sidebarOpen}
class="h-screen overflow-clip" class="h-screen overflow-clip"
{...currentModelSupportsImages ? omit(fileUpload.dropzone, ['onclick']) : {}} {...currentModelSupportsImages ? omit(fileUpload.dropzone, ['onclick']) : {}}
> >
<AppSidebar /> <AppSidebar bind:searchModalOpen />
<Sidebar.Inset class="w-full overflow-clip px-2"> <Sidebar.Inset class="w-full overflow-clip px-2">
<Tooltip> <!-- header - top left -->
{#snippet trigger(tooltip)} <div
<Sidebar.Trigger class="fixed top-3 left-2 z-50" {...tooltip.trigger}> class={cn(
<PanelLeftIcon /> 'bg-sidebar/50 fixed top-2 left-2 z-50 flex w-fit rounded-lg p-1 backdrop-blur-lg md:top-0 md:right-0 md:left-0 md:rounded-none md:rounded-br-lg',
</Sidebar.Trigger> {
{/snippet} 'md:left-(--sidebar-width)': sidebarOpen,
{cmdOrCtrl} + B 'hidden md:flex': sidebarOpen,
</Tooltip> }
)}
{#if page.params.id} >
<Tooltip> <Tooltip>
{#snippet trigger(tooltip)} {#snippet trigger(tooltip)}
<div <Sidebar.Trigger class="size-8" {...tooltip.trigger}>
class="fixed top-3 left-10 z-50 flex size-9 items-center justify-center md:top-1 md:left-auto" <PanelLeftIcon />
{...tooltip.trigger} </Sidebar.Trigger>
>
{#if currentConversationQuery.data?.public}
<LockOpenIcon class="size-4" />
{:else}
<LockIcon class="size-4" />
{/if}
</div>
{/snippet} {/snippet}
{currentConversationQuery.data?.public ? 'Public' : 'Private'} Toggle Sidebar ({cmdOrCtrl} + B)
</Tooltip> </Tooltip>
{/if}
<!-- header --> {#if page.params.id}
<div class="md:bg-sidebar fixed top-2 right-2 z-50 flex rounded-bl-lg p-1 md:top-0 md:right-0"> <Tooltip>
{#snippet trigger(tooltip)}
<div
class="z-50 flex size-8 items-center justify-center md:top-1 md:left-auto"
{...tooltip.trigger}
>
{#if currentConversationQuery.data?.public}
<LockOpenIcon class="size-4" />
{:else}
<LockIcon class="size-4" />
{/if}
</div>
{/snippet}
{currentConversationQuery.data?.public ? 'Public' : 'Private'}
</Tooltip>
{/if}
</div>
<!-- header - top right -->
<div
class={cn(
'bg-sidebar/50 fixed top-2 right-2 z-50 flex rounded-lg p-1 backdrop-blur-lg md:top-0 md:right-0 md:rounded-none md:rounded-bl-lg',
{ 'hidden md:flex': sidebarOpen }
)}
>
{#if page.params.id} {#if page.params.id}
<Tooltip> <Tooltip>
{#snippet trigger(tooltip)} {#snippet trigger(tooltip)}
@ -454,7 +489,22 @@
Share Share
</Tooltip> </Tooltip>
{/if} {/if}
<SearchModal /> <Tooltip>
{#snippet trigger(tooltip)}
<Button
onclick={openSearchModal}
variant="ghost"
size="icon"
class="size-8"
{...tooltip.trigger}
>
<SearchIcon class="!size-4" />
<span class="sr-only">Search</span>
</Button>
{/snippet}
Search ({cmdOrCtrl} + K)
</Tooltip>
<SearchModal bind:open={searchModalOpen} />
<Tooltip> <Tooltip>
{#snippet trigger(tooltip)} {#snippet trigger(tooltip)}
<Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}> <Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}>
@ -511,8 +561,17 @@
e.preventDefault(); e.preventDefault();
handleSubmit(); handleSubmit();
}} }}
bind:this={form}
> >
{#if error}
<div
in:fade={{ duration: 150 }}
class="bg-background absolute top-0 left-0 -translate-y-10 rounded-lg"
>
<div class="rounded-lg bg-red-500/50 px-2 py-0.5 text-sm text-red-400">
{error}
</div>
</div>
{/if}
{#if suggestedRules} {#if suggestedRules}
<div <div
{...popover.content} {...popover.content}

View file

@ -2,20 +2,17 @@
import { api } from '$lib/backend/convex/_generated/api'; import { api } from '$lib/backend/convex/_generated/api';
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import Modal from '$lib/components/ui/modal/modal.svelte'; import Modal from '$lib/components/ui/modal/modal.svelte';
import Tooltip from '$lib/components/ui/tooltip.svelte';
import { session } from '$lib/state/session.svelte'; import { session } from '$lib/state/session.svelte';
import { useQuery } from 'convex-svelte'; import { useQuery } from 'convex-svelte';
import { Debounced } from 'runed'; import { Debounced } from 'runed';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import SearchIcon from '~icons/lucide/search';
import { shortcut } from '$lib/actions/shortcut.svelte'; import { shortcut } from '$lib/actions/shortcut.svelte';
import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte';
let open = $state(false); let { open = $bindable(false) }: { open: boolean } = $props();
let input = $state(''); let input = $state('');
let searchMode = $state<'exact' | 'words' | 'fuzzy'>('words'); let searchMode = $state<'exact' | 'words' | 'fuzzy'>('words');
let inputEl = $state<HTMLInputElement>();
let selectedIndex = $state(-1); let selectedIndex = $state(-1);
const debouncedInput = new Debounced(() => input, 500); const debouncedInput = new Debounced(() => input, 500);
@ -82,29 +79,12 @@
<svelte:window use:shortcut={{ ctrl: true, key: 'k', callback: () => (open = true) }} /> <svelte:window use:shortcut={{ ctrl: true, key: 'k', callback: () => (open = true) }} />
<Tooltip>
{#snippet trigger(tooltip)}
<Button
onclick={() => (open = true)}
variant="ghost"
size="icon"
class="size-8"
{...tooltip.trigger}
>
<SearchIcon class="!size-4" />
<span class="sr-only">Search</span>
</Button>
{/snippet}
Search ({cmdOrCtrl} + K)
</Tooltip>
<Modal bind:open> <Modal bind:open>
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-lg font-semibold">Search Conversations</h2> <h2 class="text-lg font-semibold">Search Conversations</h2>
<div class="space-y-3"> <div class="space-y-3">
<input <input
bind:this={inputEl}
bind:value={input} bind:value={input}
onkeydown={handleKeydown} onkeydown={handleKeydown}
class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none" class="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
@ -112,7 +92,6 @@
{@attach (node) => { {@attach (node) => {
if (!open) return; if (!open) return;
setTimeout(() => { setTimeout(() => {
console.log('focus', node, open);
if (open) node.focus(); if (open) node.focus();
}, 50); }, 50);
}} }}