diff --git a/src/components/chat/ChatInterface.vue b/src/components/chat/ChatInterface.vue index c90a130..4722481 100644 --- a/src/components/chat/ChatInterface.vue +++ b/src/components/chat/ChatInterface.vue @@ -35,6 +35,8 @@ @create-collection="showCreateCollectionModal = true" @delete-session="showDeleteSessionDialog" @delete-collection="promptDeleteCollection" + @update-session-name="handleUpdateSessionName" + @share-session="handleShareSession" @agent-click=" if (!chatLoading) { handleTagAgent($event, false); @@ -67,6 +69,7 @@ >
@@ -180,6 +183,7 @@
+ + + @@ -329,6 +342,8 @@ import ConfirmModal from "../modals/ConfirmModal.vue"; import CreateCollectionModal from "../modals/CreateCollectionModal.vue"; import DeleteCollectionErrorModal from "../modals/DeleteCollectionErrorModal.vue"; import UploadModal from "../modals/UploadModal.vue"; +import ShareModal from "../modals/ShareModal.vue"; + import Header from "./elements/Header.vue"; import ChatSearchResults from "../message-handlers/ChatSearchResults.vue"; @@ -391,6 +406,14 @@ const props = defineProps({ ], }), }, + showHeader: { + type: Boolean, + default: true, + }, + showChatInput: { + type: Boolean, + default: true, + }, defaultScreenConfig: { type: Object, default: () => ({ @@ -466,6 +489,8 @@ const { deleteVideo, deleteAudio, deleteImage, + renameSession, + makeSessionPublic, } = useChatHook(props.chatHookConfig); const { @@ -531,6 +556,8 @@ const showDeleteImageDialog = ref(false); const imageToDelete = ref(null); const showDeleteCollectionErrorModal = ref(false); const deleteCollectionErrorCode = ref(null); +const showShareModal = ref(false); +const sessionToShare = ref(null); const isSetupComplete = computed(() => { return ( @@ -725,6 +752,18 @@ const confirmDeleteSession = () => { sessionToDelete.value = null; }; +const handleUpdateSessionName = async ({ sessionId: _sessionId, name }) => { + try { + await renameSession(_sessionId, name); + } catch (error) { + console.error("Error renaming session:", error?.message || error); + } +}; +const handleShareSession = (session) => { + sessionToShare.value = session; + showShareModal.value = true; +}; + // --- Upload Dialog Handlers --- const showUploadDialog = ref(false); const handleUpload = async (uploadData) => { diff --git a/src/components/chat/elements/Sidebar.vue b/src/components/chat/elements/Sidebar.vue index 1c21152..d4d930a 100644 --- a/src/components/chat/elements/Sidebar.vue +++ b/src/components/chat/elements/Sidebar.vue @@ -95,7 +95,7 @@ closeSidebar(); " :class="[ - 'vdb-c-ml-24 vdb-c-flex vdb-c-cursor-pointer vdb-c-items-center vdb-c-justify-between vdb-c-truncate vdb-c-rounded-lg vdb-c-p-8 vdb-c-text-sm vdb-c-font-medium vdb-c-text-vdb-darkishgrey', + 'vdb-c-ml-24 vdb-c-flex vdb-c-cursor-pointer vdb-c-items-center vdb-c-justify-between vdb-c-truncate vdb-c-rounded-lg vdb-c-p-8 vdb-c-px-16 vdb-c-text-sm vdb-c-font-medium vdb-c-text-vdb-darkishgrey', { 'vdb-c-bg-[#FFF5EC]': showSelectedCollection && @@ -241,37 +241,125 @@ ]" > - {{ - session.name || - new Date(session.created_at * 1000) - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/\//g, ".") - .replace(",", " -") - }} - - - + + +
+ + + + +
@@ -340,6 +428,12 @@ import CollectionIcon from "../../icons/Collection.vue"; import ComposeIcon from "../../icons/Compose.vue"; import DeleteIcon from "../../icons/Delete.vue"; import PlusIcon from "../../icons/Plus.vue"; +import CopyIcon from "../../icons/CopyIcon.vue"; +import CheckIcon from "../../icons/Check.vue"; +import DotVertical from "../../icons/DotVertical.vue"; +import EditIcon from "../../icons/Edit.vue"; +import ShareIcon from "../../icons/Share.vue"; +import Popper from "vue3-popper"; const props = defineProps({ sessions: { @@ -413,6 +507,10 @@ const hoveredSession = ref(null); const isMobile = ref(window?.innerWidth < 1024); const isOpen = ref(false); const hoveredCollection = ref(null); +const editingSessionId = ref(null); +const editingName = ref(""); +const copiedSessionId = ref(null); +const copyFeedbackTimeout = ref(null); const visibleSections = computed(() => { return props.sidebarSections; @@ -426,6 +524,8 @@ const emit = defineEmits([ "agent-click", "create-collection", "delete-collection", + "update-session-name", + "share-session", ]); const closeSidebar = () => { @@ -501,6 +601,50 @@ defineExpose({ triggerExploreAgentsFocusAnimation, toggleSidebar, }); + +const startEditing = (session) => { + editingSessionId.value = session.session_id; + editingName.value = session.name || ""; + nextTick(() => { + const input = document.getElementById(`edit-input-${session.session_id}`); + if (input) { + input.focus(); + input.select(); + } + }); +}; + +const cancelEditing = () => { + editingSessionId.value = null; + editingName.value = ""; +}; + +const saveSessionName = (session) => { + if (editingSessionId.value !== session.session_id) return; + const trimmed = (editingName.value || "").trim(); + emit("update-session-name", { sessionId: session.session_id, name: trimmed }); + cancelEditing(); +}; + +const copySessionId = async (sessionId) => { + try { + await navigator.clipboard.writeText(String(sessionId)); + copiedSessionId.value = sessionId; + if (copyFeedbackTimeout.value) { + clearTimeout(copyFeedbackTimeout.value); + } + copyFeedbackTimeout.value = setTimeout(() => { + copiedSessionId.value = null; + copyFeedbackTimeout.value = null; + }, 2000); + } catch (error) { + console.error("Failed to copy session ID", error); + } +}; + +const shareSession = (session) => { + emit("share-session", session); +}; diff --git a/src/components/hooks/useVideoDBAgent.js b/src/components/hooks/useVideoDBAgent.js index 4a4603b..cfb6d73 100644 --- a/src/components/hooks/useVideoDBAgent.js +++ b/src/components/hooks/useVideoDBAgent.js @@ -322,6 +322,76 @@ export function useVideoDBAgent(config) { }); }; + const renameSession = async (sessionId, name) => { + const trimmed = (name || "").trim(); + if (trimmed.length === 0) { + throw new Error("Session name cannot be empty."); + } + try { + const response = await fetch(`${httpUrl}/session/${sessionId}/rename`, { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: trimmed }), + }); + + const data = await response.json(); + if (!response.ok) { + const message = (data && data.message) || "Failed to rename session."; + throw new Error(message); + } + + const index = sessions.value.findIndex((s) => s.session_id === sessionId); + if (index !== -1) { + sessions.value[index] = { ...sessions.value[index], name: trimmed }; + } + + return data || { success: true }; + } catch (error) { + if (debug) + console.error("debug :videodb-chat error renaming session", error); + throw error; + } + }; + + const makeSessionPublic = async (sessionId, isPublic = true) => { + const res = {}; + try { + const response = await fetch(`${httpUrl}/session/${sessionId}/public`, { + method: "PUT", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ is_public: isPublic }), + }); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + res.status = "success"; + res.success = true; + res.data = data; + + const idx = sessions.value.findIndex((s) => s.session_id === sessionId); + if (idx !== -1) { + sessions.value[idx] = { + ...sessions.value[idx], + is_public: isPublic, + }; + } + } catch (error) { + res.status = "error"; + res.success = false; + res.error = error.message; + } + return res; + }; + const updateCollection = async () => { try { const res = await fetchCollections(); @@ -544,12 +614,6 @@ export function useVideoDBAgent(config) { const addMessage = (message) => { if (debug) console.log("debug :videodb-chat addMessage", message); if (session.isConnected) { - if (!sessions.value.some((s) => s.session_id === session.sessionId)) { - sessions.value.push({ - session_id: session.sessionId, - created_at: Date.now() / 1000, - }); - } const convId = Date.now(); const msgId = convId + 1; const _message = { @@ -566,6 +630,36 @@ export function useVideoDBAgent(config) { ...message, }; + if (!sessions.value.some((s) => s.session_id === session.sessionId)) { + const sessionData = { + session_id: session.sessionId, + message: _message, + created_at: Date.now(new Date()), + }; + fetch(`${httpUrl}/session/${session.sessionId}`, { + method: "POST", + body: JSON.stringify(sessionData), + headers: { + "Content-Type": "application/json", + }, + }) + .then((res) => res.json()) + .then((data) => { + sessions.value.push({ + session_id: data.session_id, + created_at: data.created_at, + name: data.name, + }); + + sessions.value = sessions.value.sort( + (a, b) => b.created_at - a.created_at, + ); + + session.sessionId = data.session_id; + session.name = data.name; + }); + } + conversations[convId] = { [msgId]: _message }; socket.emit("chat", _message); addClientLoadingMessage(convId); @@ -638,5 +732,7 @@ export function useVideoDBAgent(config) { uploadMedia, generateImageUrl, generateAudioUrl, + makeSessionPublic, + renameSession, }; } diff --git a/src/components/icons/DotVertical.vue b/src/components/icons/DotVertical.vue new file mode 100644 index 0000000..c15cd46 --- /dev/null +++ b/src/components/icons/DotVertical.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/components/icons/Edit.vue b/src/components/icons/Edit.vue new file mode 100644 index 0000000..798acd2 --- /dev/null +++ b/src/components/icons/Edit.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/src/components/icons/Share.vue b/src/components/icons/Share.vue new file mode 100644 index 0000000..73ca0c1 --- /dev/null +++ b/src/components/icons/Share.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/modals/ShareModal.vue b/src/components/modals/ShareModal.vue new file mode 100644 index 0000000..60c960c --- /dev/null +++ b/src/components/modals/ShareModal.vue @@ -0,0 +1,265 @@ + + + + +