styling fixes (#24)
This commit is contained in:
parent
df66ccbb2a
commit
fd9407d681
8 changed files with 179 additions and 80 deletions
|
|
@ -343,7 +343,7 @@ export const search = query({
|
|||
.query('messages')
|
||||
.withIndex('by_conversation', (q) => q.eq('conversation_id', conversation._id))
|
||||
.collect();
|
||||
|
||||
|
||||
// Search title
|
||||
const titleResults = enhancedSearch({
|
||||
needle: args.search_term,
|
||||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<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>
|
||||
<span class="text-center font-serif text-lg">Thom.chat</span>
|
||||
<div class="size-9"></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>
|
||||
{#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>
|
||||
|
|
|
|||
|
|
@ -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 }} />
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -3,15 +3,25 @@ import type { GenerateMessageRequestBody, GenerateMessageResponse } from './+ser
|
|||
|
||||
export async function callGenerateMessage(args: GenerateMessageRequestBody) {
|
||||
const res = ResultAsync.fromPromise(
|
||||
fetch('/api/generate-message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
}),
|
||||
(e) => e
|
||||
).map((r) => r.json() as Promise<GenerateMessageResponse>);
|
||||
(async () => {
|
||||
const res = await fetch('/api/generate-message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const { message } = await res.json();
|
||||
|
||||
throw new Error(message as string);
|
||||
}
|
||||
|
||||
return res.json() as Promise<GenerateMessageResponse>;
|
||||
})(),
|
||||
(e) => `${e}`
|
||||
);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,41 +409,58 @@
|
|||
</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">
|
||||
<Tooltip>
|
||||
{#snippet trigger(tooltip)}
|
||||
<Sidebar.Trigger class="fixed top-3 left-2 z-50" {...tooltip.trigger}>
|
||||
<PanelLeftIcon />
|
||||
</Sidebar.Trigger>
|
||||
{/snippet}
|
||||
{cmdOrCtrl} + B
|
||||
</Tooltip>
|
||||
|
||||
{#if page.params.id}
|
||||
<!-- 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)}
|
||||
<div
|
||||
class="fixed top-3 left-10 z-50 flex size-9 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>
|
||||
<Sidebar.Trigger class="size-8" {...tooltip.trigger}>
|
||||
<PanelLeftIcon />
|
||||
</Sidebar.Trigger>
|
||||
{/snippet}
|
||||
{currentConversationQuery.data?.public ? 'Public' : 'Private'}
|
||||
Toggle Sidebar ({cmdOrCtrl} + B)
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<!-- 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">
|
||||
{#if page.params.id}
|
||||
<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}
|
||||
<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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue