Skip to content

Commit b19e1ca

Browse files
committed
feat: Share Thread
1 parent 2dc8e80 commit b19e1ca

10 files changed

Lines changed: 162 additions & 10 deletions

File tree

apps/backend-convex/convex/http/ai.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ aiApp
238238
await c.env.runMutation(internal.threads.updateThreadInfo, { threadId, timestamp: Date.now() })
239239

240240
// // Generate new thread title
241-
// await c.env.runAction(api.threads.generateThreadTitle, { threadId, lockerKey, apiKey })
241+
// await c.env.runAction(api.threads.generateTitle, { threadId, lockerKey, apiKey })
242242

243243
controller.enqueue(encoder.encode(`o: ${JSON.stringify({ done: true })}\n`))
244244
controller.close()

apps/backend-convex/convex/threads.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const unfreeze = mutation({
182182
})
183183

184184
// Todo: maybe rate limit
185-
export const generateThreadTitle = action({
185+
export const generateTitle = action({
186186
args: {
187187
threadId: v.id('threads'),
188188
lockerKey: v.optional(v.string()),
@@ -215,6 +215,24 @@ export const generateThreadTitle = action({
215215
},
216216
})
217217

218+
export const setLockerKey = mutation({
219+
args: {
220+
threadId: v.id('threads'),
221+
newLockerKey: v.string(),
222+
},
223+
handler: async (ctx, args) => {
224+
const thread = await ctx.db.get(args.threadId)
225+
if (!thread)
226+
throw new Error('Thread not found')
227+
228+
await assertThreadAccess(ctx, { thread })
229+
230+
await ctx.db.patch(args.threadId, {
231+
lockerKey: args.newLockerKey,
232+
})
233+
},
234+
})
235+
218236
export const updateThreadInfo = internalMutation({
219237
args: {
220238
threadId: v.id('threads'),

apps/frontend/app/components/chat/ChatSidebar.vue

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@ import {
2727
useSidebar,
2828
} from '@/lib/shadcn/components/ui/sidebar'
2929
import { Button } from '~/lib/shadcn/components/ui/button'
30+
import { useToast } from '~/lib/shadcn/components/ui/toast'
3031
3132
const { $auth, $init } = useNuxtApp()
3233
const colorMode = useColorMode()
3334
const convex = useConvexClient()
3435
const chatContext = useChatContext()
3536
const sidebarContext = useSidebar()
37+
const { toast } = useToast()
38+
const { t } = useI18n()
39+
const { copy } = useClipboard({ legacy: true })
3640
3741
const threadIdRef = useThreadIdRef()
3842
@@ -116,6 +120,19 @@ async function _deleteThread(thread: Doc<'threads'>) {
116120
await deleteThread(convex, { threadId: thread._id, lockerKey: $auth.loggedIn ? undefined : thread.lockerKey })
117121
}
118122
123+
async function _shareThread(thread: Doc<'threads'>) {
124+
let lockerKey = thread.lockerKey
125+
if (!lockerKey) {
126+
const newLockerKey = getRandomLockerKey()
127+
await threadSetLockerKey(convex, { threadId: thread._id, newLockerKey })
128+
setLockerKey(thread._id, newLockerKey)
129+
lockerKey = newLockerKey
130+
}
131+
132+
await copy(`${window.location.origin}/chat/${thread._id}?lockerKey=${lockerKey}`)
133+
toast({ description: t('chat.toast.threadShareLinkCopied') })
134+
}
135+
119136
const [DefineDeleteBtn, ReuseDeleteBtn] = createReusableTemplate<{ thread: Doc<'threads'> }>()
120137
const [DefineThreadLiItem, ReuseThreadLiItem] = createReusableTemplate<{ thread: Doc<'threads'>, pinned?: boolean }>()
121138
</script>
@@ -267,6 +284,11 @@ const [DefineThreadLiItem, ReuseThreadLiItem] = createReusableTemplate<{ thread:
267284
>
268285
{{ thread.frozen ? $t('chat.thread.unfreeze') : $t('chat.thread.freeze') }}
269286
</ContextMenuItem>
287+
<ContextMenuItem @select.prevent>
288+
<ShareThreadAlertDialog :thread :callback="() => { _shareThread(thread) }" :tip-only="true">
289+
<p>{{ $t('chat.thread.share') }}</p>
290+
</ShareThreadAlertDialog>
291+
</ContextMenuItem>
270292
</ContextMenuContent>
271293
</ContextMenu>
272294
</li>
@@ -334,6 +356,10 @@ const [DefineThreadLiItem, ReuseThreadLiItem] = createReusableTemplate<{ thread:
334356
<div>InsaneUI</div>
335357
<div :class="chatContext.insaneUI.value ? ' i-hugeicons:crazy bg-mainGradient' : ' i-hugeicons:confused'" />
336358
</DropdownMenuItem>
359+
<DropdownMenuItem class="justify-between">
360+
<div>Settings</div>
361+
<div class="i-hugeicons:settings-01" />
362+
</DropdownMenuItem>
337363
</DropdownMenuContent>
338364
</DropdownMenu>
339365
</SidebarMenuItem>

apps/frontend/app/components/chat/thread/DeleteThreadAlertDialog.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ const { $auth } = useNuxtApp()
4848
</Tooltip>
4949
<AlertDialogContent>
5050
<AlertDialogHeader>
51-
<AlertDialogTitle>{{ $t('chat.sidebar.deleteThread.title') }}</AlertDialogTitle>
51+
<AlertDialogTitle>{{ $t('chat.alert.deleteThread.title') }}</AlertDialogTitle>
5252
<AlertDialogDescription class="whitespace-pre-line">
53-
{{ $t('chat.sidebar.deleteThread.description', { name: thread.title }) }}
53+
{{ $t('chat.alert.deleteThread.description', { name: thread.title }) }}
5454
</AlertDialogDescription>
5555
</AlertDialogHeader>
5656
<AlertDialogFooter>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import type { Doc } from 'backend-convex/convex/_generated/dataModel'
3+
import {
4+
AlertDialog,
5+
AlertDialogAction,
6+
AlertDialogCancel,
7+
AlertDialogContent,
8+
AlertDialogDescription,
9+
AlertDialogFooter,
10+
AlertDialogHeader,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from '@/lib/shadcn/components/ui/alert-dialog'
14+
import {
15+
Tooltip,
16+
TooltipContent,
17+
TooltipTrigger,
18+
} from '@/lib/shadcn/components/ui/tooltip'
19+
20+
const {
21+
thread,
22+
callback,
23+
tipOnly,
24+
} = defineProps<{
25+
thread: Doc<'threads'>
26+
callback: () => void
27+
tipOnly?: boolean
28+
}>()
29+
30+
const { $auth } = useNuxtApp()
31+
</script>
32+
33+
<template>
34+
<AlertDialog>
35+
<Tooltip :delay-duration="500">
36+
<AlertDialogTrigger v-show="!thread.userId || (thread.userId === $auth?.user?.sub)" as-child>
37+
<TooltipTrigger as-child @pointerdown.stop.prevent @click.shift.stop.prevent="callback()">
38+
<slot />
39+
</TooltipTrigger>
40+
<TooltipContent side="bottom" :side-offset="6">
41+
<p class="whitespace-pre-line text-center">
42+
{{ tipOnly
43+
? $t('tip.holdShift')
44+
: `${$t('chat.thread.share')}\n${$t('tip.holdShift')}` }}
45+
</p>
46+
</TooltipContent>
47+
</AlertDialogTrigger>
48+
</Tooltip>
49+
<AlertDialogContent>
50+
<AlertDialogHeader>
51+
<AlertDialogTitle>{{ $t('chat.alert.shareThread.title') }}</AlertDialogTitle>
52+
<AlertDialogDescription class="whitespace-pre-line">
53+
{{ $t('chat.alert.shareThread.description', { name: thread.title }) }}
54+
</AlertDialogDescription>
55+
</AlertDialogHeader>
56+
<AlertDialogFooter>
57+
<AlertDialogCancel>{{ $t('cancel') }}</AlertDialogCancel>
58+
<AlertDialogAction @click="callback()">
59+
{{ $t('confirm') }}
60+
</AlertDialogAction>
61+
</AlertDialogFooter>
62+
</AlertDialogContent>
63+
</AlertDialog>
64+
</template>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default defineNuxtRouteMiddleware(async (to) => {
2+
if (import.meta.server)
3+
return
4+
5+
const threadIdRef = useThreadIdRef()
6+
7+
const threadId = threadIdRef.value
8+
const lockerKey = to.query.lockerKey
9+
if (lockerKey && threadId) {
10+
if (getLockerKey(threadId) && to.query.force !== 'true')
11+
return console.error('Locker key already exists, add `&force=true` to overwrite')
12+
13+
setLockerKey(threadId, String(to.query.lockerKey))
14+
}
15+
})

apps/frontend/app/pages/chat/[...all].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SidebarProvider } from '@/lib/shadcn/components/ui/sidebar'
55
66
definePageMeta({
77
layout: 'basic',
8+
middleware: ['accept-locker-key'],
89
})
910
1011
const threadIdRef = useThreadIdRef()

apps/frontend/app/utils/chat.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,26 @@ export async function generateThreadTitle(convex: ConvexClient | ConvexHttpClien
9898
threadId,
9999
lockerKey,
100100
}: GenerateThreadTitleArgs) {
101-
await convex.action(api.threads.generateThreadTitle, {
101+
await convex.action(api.threads.generateTitle, {
102102
threadId,
103103
lockerKey,
104104
})
105105
}
106106

107+
export interface ThreadSetLockerKeyArgs {
108+
threadId: Id<'threads'>
109+
newLockerKey: string
110+
}
111+
export async function threadSetLockerKey(convex: ConvexClient | ConvexHttpClient, {
112+
threadId,
113+
newLockerKey,
114+
}: ThreadSetLockerKeyArgs) {
115+
await convex.mutation(api.threads.setLockerKey, {
116+
threadId,
117+
newLockerKey,
118+
})
119+
}
120+
107121
export interface CustomMessage extends Doc<'messages'> {
108122
id: string
109123
}

i18n.lock

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ checksums:
6161
chat.sidebar.newChat: 3d7b701878aca8e2dbb7c6732f7f3bee
6262
chat.sidebar.threads.loading: c0591fcc6aa3eb6092ab4f4ce577804c
6363
chat.sidebar.threads.empty: eaa3199ed3ba5dd2bb3988ea999649e3
64-
chat.sidebar.deleteThread.title: 3dea4a78f8c5c2073d8eab4aad3f2015
65-
chat.sidebar.deleteThread.description: b0cca6d07bbbd907b4d25dc2a1dbe2e3
64+
chat.alert.deleteThread.title: 3dea4a78f8c5c2073d8eab4aad3f2015
65+
chat.alert.deleteThread.description: b0cca6d07bbbd907b4d25dc2a1dbe2e3
6666
cancel: 2e2a849c2223911717de8caa2c71bade
6767
confirm: 90930b51154032f119fa75c1bd422d8b
6868
continue: 3cfba90b4600131e82fc4260c568d044
@@ -88,5 +88,9 @@ checksums:
8888
chat.thread.unfreeze: 0fcaf656c357870fb42132f7342aa23a
8989
tip.holdShift: 895a995e323b21fd1de51496a994103d
9090
chat.thread.frozenWithDescription: 5cf38e514472bed10297a7936924737f
91+
chat.thread.share: 3a257c0d42ffa5ed6eddfc71e46db6c0
92+
chat.alert.shareThread.title: e00e8208d2dd52a00a4cbccdc57199e3
93+
chat.alert.shareThread.description: e8eeee4bd83372c3a997a593a585f077
94+
chat.toast.threadShareLinkCopied: 301236632c59a15e329f433e7b4a3706
9195
bd59d862767f00d421bd4cacc7522c6c:
9296
hello-from-%2Fx: de51bbd5a66af7e24e90466431fa9006

locals/locales/src/sheets/frontend/i18n.csv

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ welcome,,Welcome,Bienvenido,Bienvenue,Добро пожаловать,Chào mừ
5757
chat.sidebar.newChat,,New Chat,Nuevo chat,Nouvelle conversation,Новый чат,Cuộc trò chuyện mới,新建聊天
5858
chat.sidebar.threads.loading,,Loading threads...,Cargando conversaciones...,Chargement des conversations...,Загрузка бесед...,Đang tải các cuộc trò chuyện...,正在加载对话...
5959
chat.sidebar.threads.empty,,No chats yet.,No hay chats todavía.,Pas encore de conversations.,Чатов пока нет.,Chưa có cuộc trò chuyện nào.,尚无聊天记录。
60-
chat.sidebar.deleteThread.title,,Delete Thread,Eliminar conversación,Supprimer la conversation,Удалить тему,Xóa Chủ Đề,删除话题
61-
chat.sidebar.deleteThread.description,,"Are you sure you want to delete ""{name}""?
60+
chat.alert.deleteThread.title,,Delete Thread,Eliminar conversación,Supprimer la conversation,Удалить тему,Xóa Chủ Đề,删除话题
61+
chat.alert.deleteThread.description,,"Are you sure you want to delete ""{name}""?
6262
This action cannot be undone.","¿Estás seguro de que quieres eliminar ""{name}""?
6363
Esta acción no se puede deshacer.","Êtes-vous sûr de vouloir supprimer ""{name}"" ?
6464
Cette action ne peut pas être annulée.","Вы уверены, что хотите удалить ""{name}""?
@@ -89,4 +89,14 @@ chat.thread.frozen,,Thread is frozen,El hilo está congelado,La discussion est g
8989
chat.thread.freeze,,Freeze thread,Congelar hilo,Geler la discussion,Заморозить тему,Đóng băng chủ đề,冻结线程
9090
chat.thread.unfreeze,,Unfreeze thread,Descongelar hilo,Dégeler la discussion,Разморозить тему,Giải băng chủ đề,解冻线程
9191
tip.holdShift,,"Hold ""Shift"" to skip confirmation","Mantén presionado ""Shift"" para omitir la confirmación","Maintenez ""Maj"" pour ignorer la confirmation","Удерживайте ""Shift"", чтобы пропустить подтверждение","Giữ phím ""Shift"" để bỏ qua xác nhận","按住 ""Shift"" 跳过确认"
92-
chat.thread.frozenWithDescription,,"Thread is frozen, new messages will create a new branch","El hilo está congelado, los nuevos mensajes crearán una nueva rama","La discussion est gelée, les nouveaux messages créeront une nouvelle branche","Тема заморожена, новые сообщения создадут новую ветку","Cuộc trò chuyện đã bị đóng băng, tin nhắn mới sẽ tạo một nhánh mới",线程已冻结,新消息将创建一个新分支
92+
chat.thread.frozenWithDescription,,"Thread is frozen, new messages will create a new branch","El hilo está congelado, los nuevos mensajes crearán una nueva rama","La discussion est gelée, les nouveaux messages créeront une nouvelle branche","Тема заморожена, новые сообщения создадут новую ветку","Chủ đề đã bị đóng băng, tin nhắn mới sẽ tạo một nhánh mới",线程已冻结,新消息将创建一个新分支
93+
chat.thread.share,,Share thread,Compartir hilo,Partager la discussion,Поделиться дискуссией,Chia sẻ chủ đề,共享线程
94+
chat.alert.shareThread.title,,Share Thread,Compartir hilo,Partager la discussion,Поделиться дискуссией,Chia sẻ chủ đề,共享线程
95+
chat.alert.shareThread.description,,"Are you sure you want to share ""{name}""?
96+
The recipient will have full access to the history and future of the thread.","¿Estás seguro de que quieres compartir ""{name}""?
97+
El destinatario tendrá acceso completo al historial y futuro de la conversación.","Êtes-vous sûr de vouloir partager ""{name}"" ?
98+
Le destinataire aura un accès complet à l'historique et au futur de la conversation.","Вы уверены, что хотите поделиться ""{name}""?
99+
Получатель получит полный доступ к истории и будущему этой ветки.","Bạn có chắc chắn muốn chia sẻ ""{name}""?
100+
Người nhận sẽ có toàn quyền truy cập vào lịch sử và tương lai của cuộc trò chuyện.","您确定要分享“{name}”吗?
101+
接收者将可以完全访问该线程的历史记录和未来内容。"
102+
chat.toast.threadShareLinkCopied,,The thread access link has been copied to your clipboard.,El enlace de acceso a la conversación ha sido copiado a tu portapapeles.,Le lien d'accès à la conversation a été copié dans votre presse-papiers.,Ссылка для доступа к ветке скопирована в ваш буфер обмена.,Đường dẫn truy cập cuộc trò chuyện đã được sao chép vào bảng tạm của bạn.,线程访问链接已复制到您的剪贴板。

0 commit comments

Comments
 (0)