feat(*): make message processing sequential
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { processContent } from '$lib/utils/markdown';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
images?: string[];
|
||||
mediaTypes?: string[];
|
||||
}
|
||||
|
||||
let { content, images = [], mediaTypes = [] }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="prose-mini w-full rounded-lg bg-blue-600 px-2.5 py-1.5 text-[11px] leading-relaxed text-white opacity-60"
|
||||
>
|
||||
{#if images.length > 0}
|
||||
<div class="mb-1 flex flex-wrap gap-1">
|
||||
{#each images as img, i (i)}
|
||||
<img
|
||||
src={`data:${mediaTypes[i] ?? 'image/jpeg'};base64,${img}`}
|
||||
alt=""
|
||||
class="max-h-16 rounded"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if content}
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div>{@html processContent(content)}</div>
|
||||
{/if}
|
||||
<div class="mt-1 flex items-center gap-1 text-[8px] text-blue-100/80">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-2.5 w-2.5 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.3" />
|
||||
<path
|
||||
d="M12 2a10 10 0 0 1 10 10"
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>в очереди</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,8 +52,48 @@ export const create = mutation({
|
||||
followUpOptions: v.optional(v.array(v.string())),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
},
|
||||
returns: v.id('messages'),
|
||||
returns: v.union(v.id('messages'), v.null()),
|
||||
handler: async (ctx, args) => {
|
||||
const drafts: Array<{ base64: string; mediaType: string; id: Id<'photoDrafts'> }> = [];
|
||||
if (args.photoDraftIds && args.photoDraftIds.length > 0) {
|
||||
for (const draftId of args.photoDraftIds) {
|
||||
const draft = await ctx.db.get(draftId);
|
||||
if (draft) {
|
||||
drafts.push({ base64: draft.base64, mediaType: draft.mediaType, id: draft._id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (args.source === 'web' && args.role === 'user') {
|
||||
const chat = await ctx.db.get(args.chatId);
|
||||
if (chat) {
|
||||
const pendingGenId = await ctx.db.insert('pendingGenerations', {
|
||||
userId: chat.userId,
|
||||
chatId: args.chatId,
|
||||
userMessage: args.content,
|
||||
imagesBase64: drafts.length > 0 ? drafts.map((d) => d.base64) : args.imagesBase64,
|
||||
imagesMediaTypes:
|
||||
drafts.length > 0 ? drafts.map((d) => d.mediaType) : args.imagesMediaTypes,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
for (let i = 0; i < drafts.length; i++) {
|
||||
await ctx.db.insert('pendingGenerationImages', {
|
||||
pendingGenerationId: pendingGenId,
|
||||
base64: drafts[i].base64,
|
||||
mediaType: drafts[i].mediaType,
|
||||
order: i
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const draft of drafts) {
|
||||
await ctx.db.delete(draft.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const messageId = await ctx.db.insert('messages', {
|
||||
chatId: args.chatId,
|
||||
role: args.role,
|
||||
@@ -68,16 +108,6 @@ export const create = mutation({
|
||||
isStreaming: args.isStreaming
|
||||
});
|
||||
|
||||
const drafts: Array<{ base64: string; mediaType: string; id: Id<'photoDrafts'> }> = [];
|
||||
if (args.photoDraftIds && args.photoDraftIds.length > 0) {
|
||||
for (const draftId of args.photoDraftIds) {
|
||||
const draft = await ctx.db.get(draftId);
|
||||
if (draft) {
|
||||
drafts.push({ base64: draft.base64, mediaType: draft.mediaType, id: draft._id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < drafts.length; i++) {
|
||||
await ctx.db.insert('messageImages', {
|
||||
messageId,
|
||||
@@ -87,27 +117,6 @@ export const create = mutation({
|
||||
});
|
||||
}
|
||||
|
||||
if (args.source === 'web' && args.role === 'user') {
|
||||
const chat = await ctx.db.get(args.chatId);
|
||||
if (chat) {
|
||||
const pendingGenId = await ctx.db.insert('pendingGenerations', {
|
||||
userId: chat.userId,
|
||||
chatId: args.chatId,
|
||||
userMessage: args.content,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
for (let i = 0; i < drafts.length; i++) {
|
||||
await ctx.db.insert('pendingGenerationImages', {
|
||||
pendingGenerationId: pendingGenId,
|
||||
base64: drafts[i].base64,
|
||||
mediaType: drafts[i].mediaType,
|
||||
order: i
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const draft of drafts) {
|
||||
await ctx.db.delete(draft.id);
|
||||
}
|
||||
@@ -116,6 +125,31 @@ export const create = mutation({
|
||||
}
|
||||
});
|
||||
|
||||
export const createFromBackend = mutation({
|
||||
args: {
|
||||
chatId: v.id('chats'),
|
||||
role: v.union(v.literal('user'), v.literal('assistant')),
|
||||
content: v.string(),
|
||||
source: v.union(v.literal('telegram'), v.literal('web')),
|
||||
imagesBase64: v.optional(v.array(v.string())),
|
||||
imagesMediaTypes: v.optional(v.array(v.string())),
|
||||
isStreaming: v.optional(v.boolean())
|
||||
},
|
||||
returns: v.id('messages'),
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.insert('messages', {
|
||||
chatId: args.chatId,
|
||||
role: args.role,
|
||||
content: args.content,
|
||||
source: args.source,
|
||||
imagesBase64: args.imagesBase64,
|
||||
imagesMediaTypes: args.imagesMediaTypes,
|
||||
createdAt: Date.now(),
|
||||
isStreaming: args.isStreaming
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
messageId: v.id('messages'),
|
||||
|
||||
@@ -40,6 +40,50 @@ export const list = query({
|
||||
}
|
||||
});
|
||||
|
||||
export const listByChat = query({
|
||||
args: { chatId: v.id('chats') },
|
||||
returns: v.array(
|
||||
v.object({
|
||||
_id: v.id('pendingGenerations'),
|
||||
_creationTime: v.number(),
|
||||
userMessage: v.string(),
|
||||
imagesBase64: v.optional(v.array(v.string())),
|
||||
imagesMediaTypes: v.optional(v.array(v.string())),
|
||||
createdAt: v.number()
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const pending = await ctx.db
|
||||
.query('pendingGenerations')
|
||||
.withIndex('by_chat_id', (q) => q.eq('chatId', args.chatId))
|
||||
.order('asc')
|
||||
.collect();
|
||||
|
||||
const result = [];
|
||||
for (const p of pending) {
|
||||
const images = await ctx.db
|
||||
.query('pendingGenerationImages')
|
||||
.withIndex('by_pending_generation_id', (q) => q.eq('pendingGenerationId', p._id))
|
||||
.collect();
|
||||
|
||||
const sortedImages = images.sort((a, b) => a.order - b.order);
|
||||
|
||||
result.push({
|
||||
_id: p._id,
|
||||
_creationTime: p._creationTime,
|
||||
userMessage: p.userMessage,
|
||||
imagesBase64:
|
||||
sortedImages.length > 0 ? sortedImages.map((img) => img.base64) : p.imagesBase64,
|
||||
imagesMediaTypes:
|
||||
sortedImages.length > 0 ? sortedImages.map((img) => img.mediaType) : p.imagesMediaTypes,
|
||||
createdAt: p.createdAt
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
userId: v.id('users'),
|
||||
@@ -61,6 +105,10 @@ export const remove = mutation({
|
||||
args: { id: v.id('pendingGenerations') },
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const existing = await ctx.db.get(args.id);
|
||||
if (!existing) {
|
||||
return null;
|
||||
}
|
||||
const images = await ctx.db
|
||||
.query('pendingGenerationImages')
|
||||
.withIndex('by_pending_generation_id', (q) => q.eq('pendingGenerationId', args.id))
|
||||
|
||||
@@ -65,7 +65,7 @@ export default defineSchema({
|
||||
imagesBase64: v.optional(v.array(v.string())),
|
||||
imagesMediaTypes: v.optional(v.array(v.string())),
|
||||
createdAt: v.number()
|
||||
}),
|
||||
}).index('by_chat_id', ['chatId']),
|
||||
|
||||
pendingGenerationImages: defineTable({
|
||||
pendingGenerationId: v.id('pendingGenerations'),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { api } from '$lib/convex/_generated/api';
|
||||
import type { Id } from '$lib/convex/_generated/dataModel';
|
||||
import ChatMessage from '$lib/components/ChatMessage.svelte';
|
||||
import PendingMessageBubble from '$lib/components/PendingMessageBubble.svelte';
|
||||
import ChatInput from '$lib/components/ChatInput.svelte';
|
||||
import FollowUpButtons from '$lib/components/FollowUpButtons.svelte';
|
||||
import StealthOverlay from '$lib/components/StealthOverlay.svelte';
|
||||
@@ -117,7 +118,16 @@
|
||||
: null;
|
||||
const messagesQuery = $derived(usePolling ? messagesQueryPoll! : messagesQueryWs!);
|
||||
|
||||
const pendingQueryWs = usePolling
|
||||
? null
|
||||
: useQuery(api.pendingGenerations.listByChat, () => (chatId ? { chatId } : 'skip'));
|
||||
const pendingQueryPoll = usePolling
|
||||
? usePollingQuery(api.pendingGenerations.listByChat, () => (chatId ? { chatId } : 'skip'))
|
||||
: null;
|
||||
const pendingQuery = $derived(usePolling ? pendingQueryPoll! : pendingQueryWs!);
|
||||
|
||||
let messages = $derived(messagesQuery.data ?? []);
|
||||
let pendingMessages = $derived(pendingQuery.data ?? []);
|
||||
let lastMessage = $derived(messages[messages.length - 1]);
|
||||
let followUpOptions = $derived(
|
||||
lastMessage?.role === 'assistant' && lastMessage.followUpOptions
|
||||
@@ -263,12 +273,19 @@
|
||||
|
||||
let prevMessageCount = 0;
|
||||
let prevLastMessageId: string | undefined;
|
||||
let prevPendingCount = 0;
|
||||
|
||||
$effect(() => {
|
||||
const count = messages.length;
|
||||
const lastId = lastMessage?._id;
|
||||
if (count > prevMessageCount || (lastId && lastId !== prevLastMessageId)) {
|
||||
const pendingCount = pendingMessages.length;
|
||||
if (
|
||||
count > prevMessageCount ||
|
||||
pendingCount > prevPendingCount ||
|
||||
(lastId && lastId !== prevLastMessageId)
|
||||
) {
|
||||
prevMessageCount = count;
|
||||
prevPendingCount = pendingCount;
|
||||
prevLastMessageId = lastId;
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
@@ -577,7 +594,7 @@
|
||||
{:else}
|
||||
<div class="space-y-1">
|
||||
{#each messages as message, i (message._id)}
|
||||
{#if i === messages.length - 1}
|
||||
{#if i === messages.length - 1 && pendingMessages.length === 0}
|
||||
<div bind:this={lastMessageElement}>
|
||||
<ChatMessage
|
||||
role={message.role}
|
||||
@@ -593,6 +610,23 @@
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#each pendingMessages as p, i (p._id)}
|
||||
{#if i === pendingMessages.length - 1}
|
||||
<div bind:this={lastMessageElement}>
|
||||
<PendingMessageBubble
|
||||
content={p.userMessage}
|
||||
images={p.imagesBase64 ?? []}
|
||||
mediaTypes={p.imagesMediaTypes ?? []}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<PendingMessageBubble
|
||||
content={p.userMessage}
|
||||
images={p.imagesBase64 ?? []}
|
||||
mediaTypes={p.imagesMediaTypes ?? []}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if followUpOptions.length > 0}
|
||||
|
||||
Reference in New Issue
Block a user