diff --git a/package-lock.json b/package-lock.json index 2124a7f..1c92b8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@videodb/chat-vue", - "version": "0.0.40", + "version": "0.0.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@videodb/chat-vue", - "version": "0.0.40", + "version": "0.0.41", "license": "Apache-2.0", "dependencies": { "@videodb/player-vue": "~0.0.6", @@ -17,7 +17,6 @@ "prismjs": "^1.29.0", "socket.io-client": "^4.7.5", "swiper": "^11.1.10", - "uuid": "^10.0.0", "vue3-popper": "^1.5.0" }, "devDependencies": { @@ -4404,17 +4403,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "10.0.0", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/video.js": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.19.1.tgz", diff --git a/package.json b/package.json index c1fd826..02c5e90 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "prismjs": "^1.29.0", "socket.io-client": "^4.7.5", "swiper": "^11.1.10", - "uuid": "^10.0.0", "vue3-popper": "^1.5.0" } } diff --git a/src/components/canvas-handlers/meeting-recorder/AttachBottom.vue b/src/components/canvas-handlers/meeting-recorder/AttachBottom.vue new file mode 100644 index 0000000..8e4a96e --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/AttachBottom.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/canvas-handlers/meeting-recorder/AttachLeft.vue b/src/components/canvas-handlers/meeting-recorder/AttachLeft.vue new file mode 100644 index 0000000..4824569 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/AttachLeft.vue @@ -0,0 +1,26 @@ + + diff --git a/src/components/canvas-handlers/meeting-recorder/AttachTop.vue b/src/components/canvas-handlers/meeting-recorder/AttachTop.vue new file mode 100644 index 0000000..d48ebcf --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/AttachTop.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/canvas-handlers/meeting-recorder/Button.vue b/src/components/canvas-handlers/meeting-recorder/Button.vue new file mode 100644 index 0000000..f52c5c8 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/Button.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/Cross.vue b/src/components/canvas-handlers/meeting-recorder/Cross.vue new file mode 100644 index 0000000..bbae0e8 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/Cross.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/Live.vue b/src/components/canvas-handlers/meeting-recorder/Live.vue new file mode 100644 index 0000000..0a20fc6 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/Live.vue @@ -0,0 +1,24 @@ + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/LiveAnalysisModal.vue b/src/components/canvas-handlers/meeting-recorder/LiveAnalysisModal.vue new file mode 100644 index 0000000..0b936e9 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/LiveAnalysisModal.vue @@ -0,0 +1,376 @@ + + + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/MeetingAnalysisModal.vue b/src/components/canvas-handlers/meeting-recorder/MeetingAnalysisModal.vue new file mode 100644 index 0000000..0b59dd8 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/MeetingAnalysisModal.vue @@ -0,0 +1,592 @@ + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/MeetingRecorderCanvas.vue b/src/components/canvas-handlers/meeting-recorder/MeetingRecorderCanvas.vue new file mode 100644 index 0000000..958f657 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/MeetingRecorderCanvas.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/Pinned.vue b/src/components/canvas-handlers/meeting-recorder/Pinned.vue new file mode 100644 index 0000000..011fb88 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/Pinned.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/canvas-handlers/meeting-recorder/Robot.vue b/src/components/canvas-handlers/meeting-recorder/Robot.vue new file mode 100644 index 0000000..cb0a242 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/Robot.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/canvas-handlers/meeting-recorder/UnPinned.vue b/src/components/canvas-handlers/meeting-recorder/UnPinned.vue new file mode 100644 index 0000000..3c96580 --- /dev/null +++ b/src/components/canvas-handlers/meeting-recorder/UnPinned.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/components/chat/ChatInput.vue b/src/components/chat/ChatInput.vue index 5f62847..6dcd8ec 100644 --- a/src/components/chat/ChatInput.vue +++ b/src/components/chat/ChatInput.vue @@ -119,7 +119,6 @@ @@ -1072,4 +1196,12 @@ provide("videodb-chat", { -o-animation: rotating 2s linear infinite; animation: rotating 2s linear infinite; } + +.scrollbar-hide::-webkit-scrollbar { + display: none; +} +.scrollbar-hide { + -ms-overflow-style: none; + scrollbar-width: none; +} 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/useChatInterface.js b/src/components/hooks/useChatInterface.js index dd328ba..c6b8db0 100644 --- a/src/components/hooks/useChatInterface.js +++ b/src/components/hooks/useChatInterface.js @@ -3,21 +3,57 @@ import { ref, reactive } from "vue"; export function useChatInterface() { const messageHandlers = {}; const chatInput = ref(""); - const chatAttachments = reactive([]) + const chatAttachments = reactive([]); + + // Right-side canvas state and registry + const canvasHandlers = {}; + const canvasState = reactive({ + show: false, + shrinkChat: false, + type: null, + content: null, + }); const registerMessageHandler = (contentType, handler) => { messageHandlers[contentType] = handler; }; + const registerCanvasHandler = (canvasType, handler) => { + canvasHandlers[canvasType] = handler; + }; + const setChatInput = (input) => { chatInput.value = input; }; + const setShrinkChat = (shrink) => { + canvasState.shrinkChat = shrink; + }; + + const openCanvas = (type, content) => { + canvasState.show = true; + canvasState.type = type; + canvasState.content = content || null; + }; + + const closeCanvas = () => { + canvasState.show = false; + canvasState.type = null; + canvasState.content = null; + canvasState.shrinkChat = false; + }; + return { chatInput, chatAttachments, setChatInput, + setShrinkChat, messageHandlers, registerMessageHandler, + canvasHandlers, + registerCanvasHandler, + canvasState, + openCanvas, + closeCanvas, }; } diff --git a/src/components/hooks/useVideoDBAgent.js b/src/components/hooks/useVideoDBAgent.js index 4a4603b..2d6d130 100644 --- a/src/components/hooks/useVideoDBAgent.js +++ b/src/components/hooks/useVideoDBAgent.js @@ -1,5 +1,4 @@ import io from "socket.io-client"; -import { v4 as uuidv4 } from "uuid"; import { computed, onBeforeMount, reactive, ref, toRefs, watch } from "vue"; const fetchData = async (rootUrl, endpoint) => { @@ -137,6 +136,94 @@ export function useVideoDBAgent(config) { return res; }; + const saveMeetingContext = async (msgId, context) => { + const res = {}; + try { + const response = await fetch( + `${httpUrl}/session/message/${msgId}/meeting_context`, + { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(context), + }, + ); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + res.status = "success"; + res.data = data; + } catch (error) { + res.status = "error"; + res.error = error; + } + return res; + }; + + const fetchMeetingContext = async (uiId) => { + const res = {}; + try { + const response = await fetch( + `${httpUrl}/session/meeting_context/${uiId}`, + ); + if (response.status === 404) { + res.status = "not_found"; + return res; + } + if (!response.ok) { + throw new Error("Network response was not ok"); + } + const data = await response.json(); + res.status = "success"; + res.data = data; + } catch (error) { + res.status = "error"; + res.error = error; + } + return res; + }; + + 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 refetchCollectionVideos = async () => { fetchCollectionVideos(session.collectionId).then((res) => { activeCollectionVideos.value = res.data; @@ -270,7 +357,7 @@ export function useVideoDBAgent(config) { const loadSession = (sessionId) => { let fetchPastMessages = true; if (!sessionId) { - sessionId = uuidv4(); + sessionId = crypto.randomUUID(); fetchPastMessages = false; } if (debug) console.log("debug :videodb-chat session loading", sessionId); @@ -638,5 +725,8 @@ export function useVideoDBAgent(config) { uploadMedia, generateImageUrl, generateAudioUrl, + saveMeetingContext, + fetchMeetingContext, + makeSessionPublic, }; } 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..627fecc --- /dev/null +++ b/src/components/icons/Share.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/components/message-handlers/MeetingRecorder.vue b/src/components/message-handlers/MeetingRecorder.vue new file mode 100644 index 0000000..e5a6ef9 --- /dev/null +++ b/src/components/message-handlers/MeetingRecorder.vue @@ -0,0 +1,360 @@ + + + + + diff --git a/src/components/modals/ShareModal.vue b/src/components/modals/ShareModal.vue new file mode 100644 index 0000000..1e098a4 --- /dev/null +++ b/src/components/modals/ShareModal.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/src/components/utils/index.js b/src/components/utils/index.js index 86507cb..e0238e9 100644 --- a/src/components/utils/index.js +++ b/src/components/utils/index.js @@ -1,74 +1,72 @@ -import { v1 } from 'uuid' - export function secondsToHHMMSS(val) { - if (!val) return '00:00:00' - let time = '' - time = new Date(val * 1000).toISOString().substring(11, 19) - if (time.substring(0, 2) === '00') { - return time.substring(3, time.length) + if (!val) return "00:00:00"; + let time = ""; + time = new Date(val * 1000).toISOString().substring(11, 19); + if (time.substring(0, 2) === "00") { + return time.substring(3, time.length); } - return time + return time; } export function randomHsl(num, total) { - return 'hsla(' + ((num + 1) / (total + 1)) * 360 + ', 100%, 50%, 1)' + return "hsla(" + ((num + 1) / (total + 1)) * 360 + ", 100%, 50%, 1)"; } export function separateBulletPoints(markdownString) { // Split the markdown string into individual lines - const lines = markdownString.split('\n') + const lines = markdownString.split("\n"); // Remove empty lines and trim leading/trailing whitespace from each line const cleanedLines = lines - .filter((line) => line.trim() !== '') - .map((line) => line.trim()) + .filter((line) => line.trim() !== "") + .map((line) => line.trim()); // Iterate over the cleaned lines and extract the bullet points - const bulletPoints = [] - let currentBulletPoint = '' + const bulletPoints = []; + let currentBulletPoint = ""; cleanedLines.forEach((line) => { - if (line.startsWith('-')) { + if (line.startsWith("-")) { // Add the current bullet point to the array - if (currentBulletPoint !== '') { - bulletPoints.push(currentBulletPoint.trim()) + if (currentBulletPoint !== "") { + bulletPoints.push(currentBulletPoint.trim()); } // Start a new bullet point - currentBulletPoint = line.substring(1).trim() + currentBulletPoint = line.substring(1).trim(); } else { // Append the line to the current bullet point - currentBulletPoint += ' ' + line.trim() + currentBulletPoint += " " + line.trim(); } - }) + }); // Add the last bullet point to the array - if (currentBulletPoint !== '') { - bulletPoints.push(currentBulletPoint.trim()) + if (currentBulletPoint !== "") { + bulletPoints.push(currentBulletPoint.trim()); } - return bulletPoints + return bulletPoints; } -const NOT_ALLOWED_CHARS = ['\\$', '#', '\\[', '\\]', '\\.', '/'] // Escape special characters with backslashes +const NOT_ALLOWED_CHARS = ["\\$", "#", "\\[", "\\]", "\\.", "/"]; // Escape special characters with backslashes export function generateSlug(title) { - if (title.split(' ').length > 1) { + if (title.split(" ").length > 1) { const nTitle = title - .split(' ') + .split(" ") .slice(0, 10) .filter((w) => /^[a-zA-Z0-9]+$/.test(w)) - .join('-') - title = nTitle + .join("-"); + title = nTitle; } for (const NOT_ALLOWED_CHAR of NOT_ALLOWED_CHARS) { - title = title.replace(new RegExp(NOT_ALLOWED_CHAR, 'g'), '-') + title = title.replace(new RegExp(NOT_ALLOWED_CHAR, "g"), "-"); } - const key = v1().replace(/-/g, '').substring(0, 8) - const slug = `${title}_${key}` - return slug + const key = crypto.randomUUID().replace(/-/g, "").substring(0, 8); + const slug = `${title}_${key}`; + return slug; } -export default {} +export default {};