fix share page
This commit is contained in:
parent
16f689a598
commit
31eb388f80
5 changed files with 96 additions and 97 deletions
|
|
@ -426,10 +426,7 @@
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{#if page.params.id && currentConversationQuery.data}
|
{#if page.params.id && currentConversationQuery.data}
|
||||||
<ShareButton
|
<ShareButton conversationId={page.params.id as Id<'conversations'>} />
|
||||||
conversationId={page.params.id as Id<'conversations'>}
|
|
||||||
isPublic={currentConversationQuery.data.public}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
{#snippet trigger(tooltip)}
|
{#snippet trigger(tooltip)}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@
|
||||||
import MarkdownRenderer from './markdown-renderer.svelte';
|
import MarkdownRenderer from './markdown-renderer.svelte';
|
||||||
import { ImageModal } from '$lib/components/ui/image-modal';
|
import { ImageModal } from '$lib/components/ui/image-modal';
|
||||||
import { sanitizeHtml } from '$lib/utils/markdown-it';
|
import { sanitizeHtml } from '$lib/utils/markdown-it';
|
||||||
|
import { on } from 'svelte/events';
|
||||||
|
import { isHtmlElement } from '$lib/utils/is';
|
||||||
|
|
||||||
const style = tv({
|
const style = tv({
|
||||||
base: 'prose rounded-xl p-2',
|
base: 'prose rounded-xl p-2 max-w-full',
|
||||||
variants: {
|
variants: {
|
||||||
role: {
|
role: {
|
||||||
user: 'bg-secondary/50 border border-secondary/70 px-3 py-2 !text-black/80 dark:!text-primary-foreground self-end',
|
user: 'bg-secondary/50 border border-secondary/70 px-3 py-2 !text-black/80 dark:!text-primary-foreground self-end',
|
||||||
|
|
@ -40,7 +42,23 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0 && !message.error)}
|
{#if message.role !== 'system' && !(message.role === 'assistant' && message.content.length === 0 && !message.error)}
|
||||||
<div class={cn('group flex max-w-[80%] flex-col gap-1', { 'self-end': message.role === 'user' })}>
|
<div
|
||||||
|
class={cn('group flex flex-col gap-1', { 'max-w-[80%] self-end ': message.role === 'user' })}
|
||||||
|
{@attach (node) => {
|
||||||
|
return on(node, 'click', (e) => {
|
||||||
|
const el = e.target as HTMLElement;
|
||||||
|
const closestCopyButton = el.closest('.copy[data-code]');
|
||||||
|
if (!isHtmlElement(closestCopyButton)) return;
|
||||||
|
|
||||||
|
const code = closestCopyButton.dataset.code;
|
||||||
|
if (!code) return;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code);
|
||||||
|
closestCopyButton.classList.add('copied');
|
||||||
|
setTimeout(() => closestCopyButton.classList.remove('copied'), 3000);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
{#if message.images && message.images.length > 0}
|
{#if message.images && message.images.length > 0}
|
||||||
<div class="mb-2 flex flex-wrap gap-2">
|
<div class="mb-2 flex flex-wrap gap-2">
|
||||||
{#each message.images as image (image.storage_id)}
|
{#each message.images as image (image.storage_id)}
|
||||||
|
|
@ -64,6 +82,7 @@
|
||||||
<pre class="!bg-sidebar"><code>{message.error}</code></pre>
|
<pre class="!bg-sidebar"><code>{message.error}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{:else if message.content_html}
|
{:else if message.content_html}
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
{@html sanitizeHtml(message.content_html)}
|
{@html sanitizeHtml(message.content_html)}
|
||||||
{:else}
|
{:else}
|
||||||
<svelte:boundary>
|
<svelte:boundary>
|
||||||
|
|
@ -106,9 +125,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ImageModal
|
{#if message.images && message.images.length > 0}
|
||||||
bind:open={imageModal.open}
|
<ImageModal
|
||||||
imageUrl={imageModal.imageUrl}
|
bind:open={imageModal.open}
|
||||||
fileName={imageModal.fileName}
|
imageUrl={imageModal.imageUrl}
|
||||||
/>
|
fileName={imageModal.fileName}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -210,12 +210,7 @@
|
||||||
const isMobile = new IsMobile();
|
const isMobile = new IsMobile();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if enabledArr.length === 0}
|
{#if enabledArr.length}
|
||||||
<!-- Fallback to original select if no models are loaded -->
|
|
||||||
<select bind:value={settings.modelId} class={cn('border-border border', className)}>
|
|
||||||
<option value="">Loading models...</option>
|
|
||||||
</select>
|
|
||||||
{:else}
|
|
||||||
<button
|
<button
|
||||||
{...popover.trigger}
|
{...popover.trigger}
|
||||||
class={cn(
|
class={cn(
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { api } from '$lib/backend/convex/_generated/api.js';
|
|
||||||
import { type Id } from '$lib/backend/convex/_generated/dataModel.js';
|
|
||||||
import { ConvexHttpClient } from 'convex/browser';
|
|
||||||
import { error } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
const client = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL);
|
|
||||||
|
|
||||||
export const load = async ({ params }: { params: { id: string } }) => {
|
|
||||||
try {
|
|
||||||
// Get the conversation without requiring authentication
|
|
||||||
const conversation = await client.query(api.conversations.getPublicById, {
|
|
||||||
conversation_id: params.id as Id<'conversations'>,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!conversation) {
|
|
||||||
error(404, 'Conversation not found or not shared publicly');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get messages for this conversation
|
|
||||||
const messages = await client.query(api.messages.getByConversationPublic, {
|
|
||||||
conversation_id: params.id as Id<'conversations'>,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversation,
|
|
||||||
messages,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading shared conversation:', e);
|
|
||||||
error(404, 'Conversation not found or not shared publicly');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { api } from '$lib/backend/convex/_generated/api.js';
|
||||||
|
import { type Id } from '$lib/backend/convex/_generated/dataModel.js';
|
||||||
|
import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
|
||||||
import * as Icons from '$lib/components/icons';
|
import * as Icons from '$lib/components/icons';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { LightSwitch } from '$lib/components/ui/light-switch/index.js';
|
import { LightSwitch } from '$lib/components/ui/light-switch/index.js';
|
||||||
import Tooltip from '$lib/components/ui/tooltip.svelte';
|
import Tooltip from '$lib/components/ui/tooltip.svelte';
|
||||||
import { type Doc } from '$lib/backend/convex/_generated/dataModel.js';
|
|
||||||
import Message from '../../chat/[id]/message.svelte';
|
import Message from '../../chat/[id]/message.svelte';
|
||||||
|
|
||||||
let { data }: {
|
const conversationId = page.params.id as Id<'conversations'>;
|
||||||
data: {
|
|
||||||
conversation: Doc<'conversations'>;
|
const conversationQuery = useCachedQuery(api.conversations.getPublicById, {
|
||||||
messages: Doc<'messages'>[];
|
conversation_id: conversationId,
|
||||||
}
|
});
|
||||||
} = $props();
|
|
||||||
|
const messagesQuery = useCachedQuery(api.messages.getByConversationPublic, {
|
||||||
|
conversation_id: conversationId,
|
||||||
|
});
|
||||||
|
|
||||||
const formatDate = (timestamp: number | undefined) => {
|
const formatDate = (timestamp: number | undefined) => {
|
||||||
if (!timestamp) return '';
|
if (!timestamp) return '';
|
||||||
|
|
@ -24,32 +30,29 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{data.conversation.title} | Shared Chat</title>
|
<title>{conversationQuery.data?.title || 'Shared Chat'} | Shared Chat</title>
|
||||||
<meta name="description" content="A shared conversation from Thom.chat" />
|
<meta name="description" content="A shared conversation from Thom.chat" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="min-h-screen">
|
<div class="min-h-screen">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="border-border bg-background/95 sticky top-0 z-50 border-b backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<header
|
||||||
|
class="border-border bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 border-b backdrop-blur"
|
||||||
|
>
|
||||||
<div class="mx-auto flex h-14 max-w-4xl items-center justify-between px-4">
|
<div class="mx-auto flex h-14 max-w-4xl items-center justify-between px-4">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a href="/" class="text-foreground hover:text-foreground/80 flex items-center gap-2 transition-colors">
|
<a
|
||||||
<Icons.Svelte class="size-6" />
|
href="/"
|
||||||
<span class="font-semibold">Thom.chat</span>
|
class="text-foreground hover:text-foreground/80 flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<span class="font-serif font-semibold">Thom.chat</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="text-muted-foreground text-sm">
|
<div class="text-muted-foreground text-sm">Shared conversation</div>
|
||||||
Shared conversation
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
{#snippet trigger(tooltip)}
|
{#snippet trigger(tooltip)}
|
||||||
<Button
|
<Button variant="ghost" size="sm" href="/chat" {...tooltip.trigger}>
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
href="/chat"
|
|
||||||
{...tooltip.trigger}
|
|
||||||
>
|
|
||||||
Start your own chat
|
Start your own chat
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
@ -62,40 +65,55 @@
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="mx-auto max-w-4xl px-4 py-8">
|
<main class="mx-auto max-w-4xl px-4 py-8">
|
||||||
<div class="space-y-6">
|
{#if conversationQuery.isLoading || messagesQuery.isLoading}
|
||||||
<!-- Conversation header -->
|
<div class="text-muted-foreground flex items-center justify-center py-12 text-center">
|
||||||
<div class="border-border rounded-lg border p-6">
|
<p>Loading conversation...</p>
|
||||||
<h1 class="text-foreground mb-2 text-2xl font-bold">{data.conversation.title}</h1>
|
</div>
|
||||||
<div class="text-muted-foreground flex items-center gap-4 text-sm">
|
{:else if !conversationQuery.data}
|
||||||
{#if data.conversation.updated_at}
|
<div
|
||||||
<span>Updated {formatDate(data.conversation.updated_at)}</span>
|
class="text-muted-foreground flex flex-col items-center justify-center py-12 text-center"
|
||||||
|
>
|
||||||
|
<p class="mb-2 text-lg">Conversation not found</p>
|
||||||
|
<p class="text-sm">This conversation doesn't exist or isn't shared publicly.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-6">
|
||||||
|
<!-- Conversation header -->
|
||||||
|
<div class="border-border rounded-lg border p-6">
|
||||||
|
<h1 class="text-foreground mb-2 text-2xl font-bold">{conversationQuery.data.title}</h1>
|
||||||
|
<div class="text-muted-foreground flex items-center gap-4 text-sm">
|
||||||
|
{#if conversationQuery.data.updated_at}
|
||||||
|
<span>Updated {formatDate(conversationQuery.data.updated_at)}</span>
|
||||||
|
{/if}
|
||||||
|
<span>Public conversation</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div class="flex flex-col space-y-0">
|
||||||
|
{#if messagesQuery.data && messagesQuery.data.length > 0}
|
||||||
|
{#each messagesQuery.data as message (message._id)}
|
||||||
|
<Message {message} />
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="text-muted-foreground flex flex-col items-center justify-center py-12 text-center"
|
||||||
|
>
|
||||||
|
<p class="mb-2 text-lg">No messages in this conversation yet.</p>
|
||||||
|
<p class="text-sm">The conversation appears to be empty.</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<span>Public conversation</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<!-- Messages -->
|
|
||||||
{#if data.messages && data.messages.length > 0}
|
|
||||||
<div class="space-y-4">
|
|
||||||
{#each data.messages as message (message._id)}
|
|
||||||
<div class="border-border rounded-lg border p-4">
|
|
||||||
<Message {message} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="text-muted-foreground flex flex-col items-center justify-center py-12 text-center">
|
|
||||||
<p class="mb-2 text-lg">No messages in this conversation yet.</p>
|
|
||||||
<p class="text-sm">The conversation appears to be empty.</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="border-border mt-16 border-t py-8">
|
<footer class="border-border mt-16 border-t py-8">
|
||||||
<div class="mx-auto max-w-4xl px-4">
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
<div class="text-muted-foreground flex flex-col items-center gap-4 text-center text-sm sm:flex-row sm:justify-between">
|
<div
|
||||||
|
class="text-muted-foreground flex flex-col items-center gap-4 text-center text-sm sm:flex-row sm:justify-between"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/TGlide/thom-chat"
|
href="https://github.com/TGlide/thom-chat"
|
||||||
|
|
@ -115,4 +133,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue