diff --git a/src/lib/backend/convex/messages.ts b/src/lib/backend/convex/messages.ts
index edb01f5..f6dc7a4 100644
--- a/src/lib/backend/convex/messages.ts
+++ b/src/lib/backend/convex/messages.ts
@@ -176,7 +176,7 @@ export const getByConversationPublic = query({
handler: async (ctx, args) => {
// First check if the conversation is public
const conversation = await ctx.db.get(args.conversation_id);
-
+
if (!conversation || !conversation.public) {
return null;
}
diff --git a/src/lib/backend/convex/user_rules.ts b/src/lib/backend/convex/user_rules.ts
index 07d1485..9f8acba 100644
--- a/src/lib/backend/convex/user_rules.ts
+++ b/src/lib/backend/convex/user_rules.ts
@@ -131,4 +131,4 @@ export const rename = mutation({
name: args.name,
});
},
-});
\ No newline at end of file
+});
diff --git a/src/lib/components/ui/share-button/index.ts b/src/lib/components/ui/share-button/index.ts
index ee003e6..69e41b7 100644
--- a/src/lib/components/ui/share-button/index.ts
+++ b/src/lib/components/ui/share-button/index.ts
@@ -1 +1 @@
-export { default as ShareButton } from './share-button.svelte';
\ No newline at end of file
+export { default as ShareButton } from './share-button.svelte';
diff --git a/src/lib/components/ui/share-button/share-button.svelte b/src/lib/components/ui/share-button/share-button.svelte
index aae5ced..080654a 100644
--- a/src/lib/components/ui/share-button/share-button.svelte
+++ b/src/lib/components/ui/share-button/share-button.svelte
@@ -142,4 +142,3 @@
{/if}
-
diff --git a/src/lib/components/ui/sidebar/sidebar-sidebar.svelte b/src/lib/components/ui/sidebar/sidebar-sidebar.svelte
index acac6a6..d24f83a 100644
--- a/src/lib/components/ui/sidebar/sidebar-sidebar.svelte
+++ b/src/lib/components/ui/sidebar/sidebar-sidebar.svelte
@@ -12,7 +12,7 @@
diff --git a/src/lib/utils/rules.ts b/src/lib/utils/rules.ts
index 29c51e4..fca1192 100644
--- a/src/lib/utils/rules.ts
+++ b/src/lib/utils/rules.ts
@@ -1,6 +1,9 @@
-import type { Doc } from "$lib/backend/convex/_generated/dataModel";
+import type { Doc } from '$lib/backend/convex/_generated/dataModel';
-export function parseMessageForRules(message: string, rules: Doc<'user_rules'>[]): Doc<'user_rules'>[] {
+export function parseMessageForRules(
+ message: string,
+ rules: Doc<'user_rules'>[]
+): Doc<'user_rules'>[] {
const matchedRules: Doc<'user_rules'>[] = [];
for (const rule of rules) {
@@ -11,4 +14,4 @@ export function parseMessageForRules(message: string, rules: Doc<'user_rules'>[]
}
return matchedRules;
-}
\ No newline at end of file
+}
diff --git a/src/routes/chat/[id]/message.svelte b/src/routes/chat/[id]/message.svelte
index 07ab0ad..d56deea 100644
--- a/src/routes/chat/[id]/message.svelte
+++ b/src/routes/chat/[id]/message.svelte
@@ -163,7 +163,7 @@
- import { goto } from '$app/navigation';
- import { page } from '$app/state';
- import { api } from '$lib/backend/convex/_generated/api.js';
- import { type Doc, type Id } from '$lib/backend/convex/_generated/dataModel.js';
- import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
- import AppSidebar from '$lib/components/app-sidebar.svelte';
- import * as Icons from '$lib/components/icons';
- import { Button } from '$lib/components/ui/button';
- import { ImageModal } from '$lib/components/ui/image-modal';
- import { LightSwitch } from '$lib/components/ui/light-switch/index.js';
- import { ShareButton } from '$lib/components/ui/share-button';
- import * as Sidebar from '$lib/components/ui/sidebar';
- import Tooltip from '$lib/components/ui/tooltip.svelte';
- import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js';
- import { TextareaAutosize } from '$lib/spells/textarea-autosize.svelte.js';
- import { models } from '$lib/state/models.svelte';
- import { usePrompt } from '$lib/state/prompt.svelte.js';
- import { session } from '$lib/state/session.svelte.js';
- import { settings } from '$lib/state/settings.svelte.js';
- import { Provider } from '$lib/types';
- import { compressImage } from '$lib/utils/image-compression';
- import { supportsImages } from '$lib/utils/model-capabilities';
- import { omit, pick } from '$lib/utils/object.js';
- import { cn } from '$lib/utils/utils.js';
- import { useConvexClient } from 'convex-svelte';
- import { FileUpload, Popover } from 'melt/builders';
- import { Debounced, ElementSize, IsMounted, PersistedState, ScrollState } from 'runed';
- import { fade, scale } from 'svelte/transition';
- import SendIcon from '~icons/lucide/arrow-up';
- import ChevronDownIcon from '~icons/lucide/chevron-down';
- import ImageIcon from '~icons/lucide/image';
- import PanelLeftIcon from '~icons/lucide/panel-left';
- import SearchIcon from '~icons/lucide/search';
- import Settings2Icon from '~icons/lucide/settings-2';
- 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 { callGenerateMessage } from '../api/generate-message/call.js';
- import ModelPicker from './model-picker.svelte';
- import SearchModal from './search-modal.svelte';
- import SparkleIcon from '~icons/lucide/sparkle';
- import { callEnhancePrompt } from '../api/enhance-prompt/call.js';
- import ShinyText from '$lib/components/animations/shiny-text.svelte';
-
- const client = useConvexClient();
-
- let { children } = $props();
-
- let textarea = $state
();
- let abortController = $state(null);
-
- $effect(() => {
- client.mutation(api.user_enabled_models.enable_initial, {
- session_token: session.current?.session.token ?? '',
- });
- });
-
- const currentConversationQuery = useCachedQuery(api.conversations.getById, () => ({
- conversation_id: page.params.id as Id<'conversations'>,
- session_token: session.current?.session.token ?? '',
- }));
-
- const isGenerating = $derived(
- Boolean(currentConversationQuery.data?.generating) || currentConversationQuery.isLoading
- );
-
- async function stopGeneration() {
- if (!page.params.id || !session.current?.session.token) return;
-
- try {
- const result = await callCancelGeneration({
- conversation_id: page.params.id,
- session_token: session.current.session.token,
- });
-
- if (result.isErr()) {
- console.error('Failed to cancel generation:', result.error);
- } else {
- console.log('Generation cancelled:', result.value.cancelled);
- }
- } catch (error) {
- console.error('Error cancelling generation:', error);
- }
-
- // Clear local abort controller if it exists
- if (abortController) {
- abortController = null;
- }
- }
-
- let loading = $state(false);
-
- let enhancingPrompt = $state(false);
-
- const textareaDisabled = $derived(
- isGenerating ||
- loading ||
- (currentConversationQuery.data &&
- currentConversationQuery.data.user_id !== session.current?.user.id) ||
- enhancingPrompt
- );
-
- let error = $state(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;
-
- loading = true;
-
- const imagesCopy = [...selectedImages];
- selectedImages = [];
-
- try {
- const res = await callGenerateMessage({
- message: message.current,
- session_token: session.current?.session.token,
- conversation_id: page.params.id ?? undefined,
- model_id: settings.modelId,
- images: imagesCopy.length > 0 ? imagesCopy : undefined,
- web_search_enabled: settings.webSearchEnabled,
- });
-
- if (res.isErr()) {
- error = res._unsafeUnwrapErr() ?? 'An unknown error occurred';
- return;
- }
-
- const cid = res.value.conversation_id;
-
- if (page.params.id !== cid) {
- goto(`/chat/${cid}`);
- }
- } catch (error) {
- console.error('Error generating message:', error);
- } finally {
- loading = false;
- message.current = '';
- }
- }
-
- let abortEnhance: AbortController | null = $state(null);
-
- async function enhancePrompt() {
- if (!session.current?.session.token) return;
-
- enhancingPrompt = true;
-
- abortEnhance = new AbortController();
-
- const res = await callEnhancePrompt(
- {
- prompt: message.current,
- },
- {
- signal: abortEnhance.signal,
- }
- );
-
- if (res.isErr()) {
- const e = res.error;
-
- if (e.toLowerCase().includes('aborterror')) {
- enhancingPrompt = false;
- return;
- }
-
- error = res._unsafeUnwrapErr() ?? 'An unknown error occurred while enhancing the prompt';
-
- enhancingPrompt = false;
- return;
- }
-
- message.current = res.value.enhanced_prompt;
-
- enhancingPrompt = false;
- }
-
- const rulesQuery = useCachedQuery(api.user_rules.all, {
- session_token: session.current?.session.token ?? '',
- });
-
- const autosize = new TextareaAutosize();
-
- const message = new PersistedState('prompt', '');
- let selectedImages = $state<{ url: string; storage_id: string; fileName?: string }[]>([]);
- let isUploading = $state(false);
- let fileInput = $state();
- let imageModal = $state<{ open: boolean; imageUrl: string; fileName: string }>({
- open: false,
- imageUrl: '',
- fileName: '',
- });
-
- usePrompt(
- () => message.current,
- (v) => (message.current = v)
- );
-
- models.init();
-
- const currentModelSupportsImages = $derived.by(() => {
- if (!settings.modelId) return false;
- const openRouterModels = models.from(Provider.OpenRouter);
- const currentModel = openRouterModels.find((m) => m.id === settings.modelId);
- return currentModel ? supportsImages(currentModel) : false;
- });
-
- const fileUpload = new FileUpload({
- multiple: true,
- accept: 'image/*',
- maxSize: 10 * 1024 * 1024, // 10MB
- });
-
- async function handleFileChange(files: File[]) {
- if (!files.length || !session.current?.session.token) return;
-
- isUploading = true;
- const uploadedFiles: { url: string; storage_id: string; fileName?: string }[] = [];
-
- try {
- for (const file of files) {
- // Skip non-image files
- if (!file.type.startsWith('image/')) {
- console.warn('Skipping non-image file:', file.name);
- continue;
- }
-
- // Compress image to max 1MB
- const compressedFile = await compressImage(file, 1024 * 1024);
-
- // Generate upload URL
- const uploadUrl = await client.mutation(api.storage.generateUploadUrl, {
- session_token: session.current.session.token,
- });
-
- // Upload compressed file
- const result = await fetch(uploadUrl, {
- method: 'POST',
- body: compressedFile,
- });
-
- if (!result.ok) {
- throw new Error(`Upload failed: ${result.statusText}`);
- }
-
- const { storageId } = await result.json();
-
- // Get the URL for the uploaded file
- const url = await client.query(api.storage.getUrl, {
- storage_id: storageId,
- session_token: session.current.session.token,
- });
-
- if (url) {
- uploadedFiles.push({ url, storage_id: storageId, fileName: file.name });
- }
- }
-
- selectedImages = [...selectedImages, ...uploadedFiles];
- } catch (error) {
- console.error('Upload failed:', error);
- } finally {
- isUploading = false;
- }
- }
-
- function removeImage(index: number) {
- selectedImages = selectedImages.filter((_, i) => i !== index);
- }
-
- function openImageModal(imageUrl: string, fileName: string) {
- imageModal = {
- open: true,
- imageUrl,
- fileName,
- };
- }
-
- $effect(() => {
- if (fileUpload.selected.size > 0) {
- handleFileChange(Array.from(fileUpload.selected));
- fileUpload.clear();
- }
- });
-
- const suggestedRules = $derived.by(() => {
- if (!rulesQuery.data || rulesQuery.data.length === 0) return;
- if (!textarea) return;
-
- const cursor = textarea.selectionStart;
-
- const index = message.current.lastIndexOf('@', cursor);
- if (index === -1) return;
-
- const ruleFromCursor = message.current.slice(index + 1, cursor);
-
- const suggestions: Doc<'user_rules'>[] = [];
-
- for (const rule of rulesQuery.data) {
- // on a match, don't show any suggestions
- if (rule.name === ruleFromCursor) return;
-
- if (rule.name.toLowerCase().startsWith(ruleFromCursor.toLowerCase())) {
- suggestions.push(rule);
- }
- }
-
- return suggestions.length > 0 ? suggestions : undefined;
- });
-
- const popover = new Popover({
- floatingConfig: {
- computePosition: { placement: 'top' },
- },
- });
-
- function completeRule(rule: Doc<'user_rules'>) {
- if (!textarea) return;
-
- const cursor = textarea.selectionStart;
-
- const index = message.current.lastIndexOf('@', cursor);
- if (index === -1) return;
-
- message.current =
- message.current.slice(0, index) + `@${rule.name}` + message.current.slice(cursor);
- textarea.selectionStart = index + rule.name.length + 1;
- textarea.selectionEnd = index + rule.name.length + 1;
-
- popover.open = false;
- }
-
- function completeSelectedRule() {
- if (!suggestedRules) return;
-
- const rules = Array.from(ruleList.querySelectorAll('[data-list-item]'));
-
- const activeIndex = rules.findIndex((r) => r.getAttribute('data-active') === 'true');
- if (activeIndex === -1) return;
-
- const rule = suggestedRules[activeIndex];
-
- if (!rule) return;
-
- completeRule(rule);
- }
-
- let ruleList = $state(null!);
-
- function handleKeyboardNavigation(direction: 'up' | 'down') {
- if (!suggestedRules) return;
-
- const rules = Array.from(ruleList.querySelectorAll('[data-list-item]'));
-
- let activeIndex = rules?.findIndex((r) => r.getAttribute('data-active') === 'true');
- if (activeIndex === -1) {
- if (!suggestedRules[0]) return;
-
- rules[0]?.setAttribute('data-active', 'true');
- return;
- }
-
- // don't loop
- if (direction === 'up' && activeIndex === 0) {
- return;
- }
- // don't loop
- if (direction === 'down' && activeIndex === suggestedRules.length - 1) {
- return;
- }
-
- rules[activeIndex]?.setAttribute('data-active', 'false');
-
- if (direction === 'up') {
- const newIndex = activeIndex - 1;
- if (!suggestedRules[newIndex]) return;
-
- rules[newIndex]?.setAttribute('data-active', 'true');
- } else {
- const newIndex = activeIndex + 1;
- if (!suggestedRules[newIndex]) return;
-
- rules[newIndex]?.setAttribute('data-active', 'true');
- }
- }
-
- const textareaSize = new ElementSize(() => textarea);
-
- let textareaWrapper = $state();
- const wrapperSize = new ElementSize(() => textareaWrapper);
-
- let conversationList = $state();
- const scrollState = new ScrollState({
- element: () => conversationList,
- });
-
- const mounted = new IsMounted();
-
- const notAtBottom = new Debounced(
- () => (mounted.current ? !scrollState.arrived.bottom : false),
- () => (mounted.current ? 250 : 0)
- );
-
- let searchModalOpen = $state(false);
-
- function openSearchModal() {
- searchModalOpen = true;
- }
-
- let sidebarOpen = $state(false);
-
-
-
-
-
-
- {#if !sidebarOpen}
-
-
-
- {#snippet trigger(tooltip)}
-
-
-
- {/snippet}
- Toggle Sidebar ({cmdOrCtrl} + B)
-
-
- {/if}
-
-
-
-
-
-
- {@render children()}
-
-
-
-
-
-
-
-
-
-
-
- {#if fileUpload.isDragging && currentModelSupportsImages}
-
-
-
-
Add image
-
Drop an image here to attach it to your message.
-
-
- {/if}
-
-
-
-
-