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 (titleResults.length > 0 || messageResults.length > 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 titleScore = titleResults.length > 0 ? (titleResults[0]?.score ?? 0) : 0;
const messageScore =
messageResults.length > 0 ? Math.max(...messageResults.map((r) => r.score)) : 0;
results.push({
conversation,
messages: messageResults.map(r => r.item),
messages: messageResults.map((r) => r.item),
score: Math.max(titleScore, messageScore),
titleMatch: titleResults.length > 0,
});

View file

@ -62,6 +62,9 @@ export const create = mutation({
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
// const messages = await ctx.runQuery(api.messages.getAllFromConversation, {

View file

@ -1,5 +1,6 @@
<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 Tooltip from '$lib/components/ui/tooltip.svelte';
import { cn } from '$lib/utils/utils.js';
@ -17,6 +18,10 @@
import XIcon from '~icons/lucide/x';
import { page } from '$app/state';
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();
@ -115,13 +120,28 @@
{ key: 'lastMonth', label: 'Last 30 days', conversations: groupedConversations.lastMonth },
{ key: 'older', label: 'Older', conversations: groupedConversations.older },
]);
function openSearchModal() {
searchModalOpen = true;
}
</script>
<Sidebar class="flex flex-col overflow-clip p-2">
<div class="flex place-items-center justify-center py-2">
<span class="text-center font-serif text-lg">Thom.chat</span>
<Sidebar.Sidebar class="flex flex-col overflow-clip p-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>
<div class="mt-1 flex w-full px-2">
<span class="text-center font-serif text-lg">Thom.chat</span>
<div class="size-9"></div>
</div>
<div class="mt-1 flex w-full flex-col gap-2 px-2">
<Tooltip>
{#snippet trigger(tooltip)}
<a
@ -134,7 +154,21 @@
New Chat
</a>
{/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>
</div>
<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>
{/if}
</div>
</Sidebar>
</Sidebar.Sidebar>

View file

@ -4,9 +4,17 @@
import { useSidebar } from './sidebar.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();
$effect(() => {
open = sidebar.showSidebar;
});
</script>
<svelte:window use:shortcut={{ key: 'b', ctrl: true, callback: sidebar.toggle }} />

View file

@ -44,11 +44,16 @@ export class SidebarSidebarState {
export class SidebarControlState {
constructor(readonly root: SidebarRootState) {
this.closeMobile = this.closeMobile.bind(this);
this.toggle = this.toggle.bind(this);
}
closeMobile() {
this.root.closeMobile();
}
toggle() {
this.root.toggle();
}
}
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) {
const res = ResultAsync.fromPromise(
fetch('/api/generate-message', {
(async () => {
const res = await fetch('/api/generate-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
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;
}

View file

@ -32,25 +32,25 @@
import CheckIcon from '~icons/lucide/check';
import ChevronDownIcon from '~icons/lucide/chevron-down';
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 SearchIcon from '~icons/lucide/search';
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 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 { callCancelGeneration } from '../api/cancel-generation/call.js';
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';
const client = useConvexClient();
let { children } = $props();
let form = $state<HTMLFormElement>();
let textarea = $state<HTMLTextAreaElement>();
let abortController = $state<AbortController | null>(null);
@ -89,11 +89,20 @@
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() {
if (isGenerating) return;
error = null;
// TODO: Re-use zod here from server endpoint for better error messages?
if (message.current === '' || !session.current?.user.id || !settings.modelId) return;
@ -113,7 +122,8 @@
});
if (res.isErr()) {
return; // TODO: Handle error
error = res._unsafeUnwrapErr() ?? 'An unknown error occurred';
return;
}
const cid = res.value.conversation_id;
@ -384,6 +394,14 @@
() => !scrollState.arrived.bottom,
() => (mounted.current ? 250 : 0)
);
let searchModalOpen = $state(false);
function openSearchModal() {
searchModalOpen = true;
}
let sidebarOpen = $state(false);
</script>
<svelte:head>
@ -391,26 +409,37 @@
</svelte:head>
<Sidebar.Root
bind:open={sidebarOpen}
class="h-screen overflow-clip"
{...currentModelSupportsImages ? omit(fileUpload.dropzone, ['onclick']) : {}}
>
<AppSidebar />
<AppSidebar bind:searchModalOpen />
<Sidebar.Inset class="w-full overflow-clip px-2">
<!-- header - top left -->
<div
class={cn(
'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',
{
'md:left-(--sidebar-width)': sidebarOpen,
'hidden md:flex': sidebarOpen,
}
)}
>
<Tooltip>
{#snippet trigger(tooltip)}
<Sidebar.Trigger class="fixed top-3 left-2 z-50" {...tooltip.trigger}>
<Sidebar.Trigger class="size-8" {...tooltip.trigger}>
<PanelLeftIcon />
</Sidebar.Trigger>
{/snippet}
{cmdOrCtrl} + B
Toggle Sidebar ({cmdOrCtrl} + B)
</Tooltip>
{#if page.params.id}
<Tooltip>
{#snippet trigger(tooltip)}
<div
class="fixed top-3 left-10 z-50 flex size-9 items-center justify-center md:top-1 md:left-auto"
class="z-50 flex size-8 items-center justify-center md:top-1 md:left-auto"
{...tooltip.trigger}
>
{#if currentConversationQuery.data?.public}
@ -423,9 +452,15 @@
{currentConversationQuery.data?.public ? 'Public' : 'Private'}
</Tooltip>
{/if}
</div>
<!-- header -->
<div class="md:bg-sidebar fixed top-2 right-2 z-50 flex rounded-bl-lg p-1 md:top-0 md:right-0">
<!-- 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}
<Tooltip>
{#snippet trigger(tooltip)}
@ -454,7 +489,22 @@
Share
</Tooltip>
{/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>
{#snippet trigger(tooltip)}
<Button variant="ghost" size="icon" class="size-8" href="/account" {...tooltip.trigger}>
@ -511,8 +561,17 @@
e.preventDefault();
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}
<div
{...popover.content}

View file

@ -2,20 +2,17 @@
import { api } from '$lib/backend/convex/_generated/api';
import { Button } from '$lib/components/ui/button/index.js';
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 { useQuery } from 'convex-svelte';
import { Debounced } from 'runed';
import { tick } from 'svelte';
import { goto } from '$app/navigation';
import SearchIcon from '~icons/lucide/search';
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 searchMode = $state<'exact' | 'words' | 'fuzzy'>('words');
let inputEl = $state<HTMLInputElement>();
let selectedIndex = $state(-1);
const debouncedInput = new Debounced(() => input, 500);
@ -82,29 +79,12 @@
<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>
<div class="space-y-4">
<h2 class="text-lg font-semibold">Search Conversations</h2>
<div class="space-y-3">
<input
bind:this={inputEl}
bind:value={input}
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"
@ -112,7 +92,6 @@
{@attach (node) => {
if (!open) return;
setTimeout(() => {
console.log('focus', node, open);
if (open) node.focus();
}, 50);
}}