cache the summary

This commit is contained in:
Pranav
2026-01-29 11:45:36 -08:00
parent 137f0e726b
commit f45e82016d
14 changed files with 245 additions and 108 deletions
@@ -37,14 +37,17 @@ class TasksAPI extends ApiClient {
/**
* Summarizes a conversation.
* @param {string} conversationId - The conversation ID to summarize.
* @param {AbortSignal} [signal] - AbortSignal to cancel the request.
* @param {Object} [options] - Additional options.
* @param {boolean} [options.forceRegenerate] - Force regeneration of cached summary.
* @param {AbortSignal} [options.signal] - AbortSignal to cancel the request.
* @returns {Promise} A promise that resolves with the summary.
*/
summarize(conversationId, signal) {
summarize(conversationId, { forceRegenerate = false, signal } = {}) {
return axios.post(
`${this.url}/summarize`,
{
conversation_display_id: conversationId,
force_regenerate: forceRegenerate,
},
{ signal }
);
@@ -350,7 +350,7 @@ const deleteConversation = () => {
:class="showMetaSection ? 'top-8' : 'top-4'"
>
<div class="flex items-center gap-1 ml-auto">
<ConversationSummary ref="summaryRef" :conversation-id="chat.id" />
<ConversationSummary ref="summaryRef" :chat="chat" />
<span class="font-normal leading-4 text-xxs">
<TimeAgo
:last-activity-timestamp="chat.timestamp"
@@ -1,6 +1,7 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -8,38 +9,56 @@ import TasksAPI from 'dashboard/api/captain/tasks';
import Button from 'dashboard/components-next/button/Button.vue';
const props = defineProps({
conversationId: {
type: [Number, String],
chat: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const store = useStore();
const { isCloudFeatureEnabled } = useAccount();
const { formatMessage } = useMessageFormatter();
const isExpanded = ref(false);
const isLoading = ref(false);
const summary = ref('');
const error = ref('');
const captainTasksEnabled = computed(() => {
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
});
const cachedSummary = computed(() => props.chat?.cached_summary || '');
const cachedSummaryAt = computed(() => props.chat?.cached_summary_at || 0);
const lastActivityAt = computed(() => props.chat?.last_activity_at || 0);
const isStale = computed(() => {
if (!cachedSummaryAt.value) return true;
return lastActivityAt.value > cachedSummaryAt.value;
});
const formattedSummary = computed(() => {
return summary.value ? formatMessage(summary.value) : '';
return cachedSummary.value ? formatMessage(cachedSummary.value) : '';
});
const fetchSummary = async () => {
isLoading.value = true;
error.value = '';
try {
const result = await TasksAPI.summarize(props.conversationId);
const result = await TasksAPI.summarize(props.chat.id, {
forceRegenerate: false,
});
const {
data: { message: generatedMessage },
data: { message: generatedSummary },
} = result;
summary.value = generatedMessage || '';
if (generatedSummary) {
store.dispatch('updateConversationCachedSummary', {
conversationId: props.chat.id,
cachedSummary: generatedSummary,
cachedSummaryAt: Math.floor(Date.now() / 1000),
});
}
} catch (e) {
if (e.name !== 'AbortError' && e.name !== 'CanceledError') {
error.value = e.response?.data?.error || t('CHAT_LIST.SUMMARY.ERROR');
@@ -55,7 +74,8 @@ const toggleSummary = async () => {
return;
}
isExpanded.value = true;
if (!summary.value && !error.value) {
// Only fetch if no cached summary
if (!cachedSummary.value && !error.value) {
await fetchSummary();
}
};
@@ -68,10 +88,10 @@ const onButtonClick = e => {
defineExpose({
isExpanded,
isLoading,
summary,
error,
formattedSummary,
captainTasksEnabled,
isStale,
});
</script>
@@ -359,7 +359,9 @@
"DESCRIPTION": "Generate an AI-powered summary of this conversation",
"GENERATE": "Generate Summary",
"REGENERATE": "Regenerate",
"REFRESH": "Refresh",
"RETRY": "Retry",
"STALE": "Summary may be outdated",
"ERROR": "Failed to generate summary",
"EMPTY": "No summary available"
},
@@ -1,6 +1,7 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -16,35 +17,56 @@ const props = defineProps({
});
const { t } = useI18n();
const store = useStore();
const { isCloudFeatureEnabled } = useAccount();
const { formatMessage } = useMessageFormatter();
const currentChat = useMapGetter('getSelectedChat');
const isLoading = ref(false);
const summary = ref('');
const error = ref('');
const hasFetched = ref(false);
const captainTasksEnabled = computed(() => {
return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN_TASKS);
});
const formattedSummary = computed(() => {
return summary.value ? formatMessage(summary.value) : '';
const cachedSummary = computed(() => currentChat.value?.cached_summary || '');
const cachedSummaryAt = computed(
() => currentChat.value?.cached_summary_at || 0
);
const lastActivityAt = computed(() => currentChat.value?.last_activity_at || 0);
const isStale = computed(() => {
if (!cachedSummaryAt.value) return true;
return lastActivityAt.value > cachedSummaryAt.value;
});
const fetchSummary = async () => {
if (!captainTasksEnabled.value || hasFetched.value) return;
const hasSummary = computed(() => !!cachedSummary.value);
const formattedSummary = computed(() => {
return cachedSummary.value ? formatMessage(cachedSummary.value) : '';
});
const fetchSummary = async (forceRegenerate = false) => {
if (!captainTasksEnabled.value) return;
isLoading.value = true;
error.value = '';
hasFetched.value = true;
try {
const result = await TasksAPI.summarize(props.conversationId);
const result = await TasksAPI.summarize(props.conversationId, {
forceRegenerate,
});
const {
data: { message: generatedMessage },
data: { message: generatedSummary },
} = result;
summary.value = generatedMessage || '';
if (generatedSummary) {
store.dispatch('updateConversationCachedSummary', {
conversationId: currentChat.value.id,
cachedSummary: generatedSummary,
cachedSummaryAt: Math.floor(Date.now() / 1000),
});
}
} catch (e) {
if (e.name !== 'AbortError' && e.name !== 'CanceledError') {
error.value =
@@ -55,18 +77,11 @@ const fetchSummary = async () => {
}
};
const refetch = () => {
hasFetched.value = false;
summary.value = '';
error.value = '';
fetchSummary();
};
const regenerate = () => fetchSummary(true);
watch(
() => props.conversationId,
() => {
hasFetched.value = false;
summary.value = '';
error.value = '';
}
);
@@ -74,6 +89,8 @@ watch(
defineExpose({
fetchSummary,
captainTasksEnabled,
cachedSummary,
isStale,
});
</script>
@@ -90,11 +107,11 @@ defineExpose({
size="sm"
variant="link"
class="ml-2"
@click="refetch"
@click="() => fetchSummary(true)"
/>
</div>
<div v-else-if="!hasFetched" class="flex flex-col items-center gap-3 py-2">
<div v-else-if="!hasSummary" class="flex flex-col items-center gap-3 py-2">
<p class="text-sm text-n-slate-11 text-center mb-0">
{{ t('CONVERSATION_SIDEBAR.SUMMARY.DESCRIPTION') }}
</p>
@@ -102,49 +119,37 @@ defineExpose({
:label="t('CONVERSATION_SIDEBAR.SUMMARY.GENERATE')"
icon="i-material-symbols-auto-awesome"
size="sm"
@click="fetchSummary"
@click="() => fetchSummary(false)"
/>
</div>
<div
v-else-if="summary"
class="summary-content text-sm text-n-slate-11 animate-fade-in [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-1 [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_strong]:text-n-slate-12"
v-html="formattedSummary"
/>
<div v-else class="text-sm text-n-slate-11 py-2">
{{ t('CONVERSATION_SIDEBAR.SUMMARY.EMPTY') }}
</div>
<div
v-if="hasFetched && !isLoading && !error"
class="mt-3 pt-3 border-t border-n-weak"
>
<Button
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REGENERATE')"
icon="i-lucide-refresh-cw"
size="sm"
variant="faded"
color="slate"
@click="refetch"
<template v-else>
<div
v-if="isStale"
class="flex items-center gap-2 mb-2 text-xs text-n-amber-11"
>
<span>{{ t('CONVERSATION_SIDEBAR.SUMMARY.STALE') }}</span>
<Button
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REFRESH')"
size="xs"
variant="link"
@click="regenerate"
/>
</div>
<div
class="summary-content text-sm text-n-slate-11 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:my-1 [&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0 [&_strong]:text-n-slate-12"
v-html="formattedSummary"
/>
</div>
<div class="mt-3 pt-3 border-t border-n-weak">
<Button
:label="t('CONVERSATION_SIDEBAR.SUMMARY.REGENERATE')"
icon="i-lucide-refresh-cw"
size="sm"
variant="faded"
color="slate"
@click="regenerate"
/>
</div>
</template>
</div>
</template>
<style scoped>
.animate-fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@@ -412,6 +412,17 @@ const actions = {
});
},
updateConversationCachedSummary(
{ commit },
{ conversationId, cachedSummary, cachedSummaryAt }
) {
commit(types.UPDATE_CONVERSATION_CACHED_SUMMARY, {
conversationId,
cachedSummary,
cachedSummaryAt,
});
},
setChatStatusFilter({ commit }, data) {
commit(types.CHANGE_CHAT_STATUS_FILTER, data);
},
@@ -116,6 +116,16 @@ export const mutations = {
chat.last_activity_at = lastActivityAt;
}
},
[types.UPDATE_CONVERSATION_CACHED_SUMMARY](
_state,
{ conversationId, cachedSummary, cachedSummaryAt }
) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
if (chat) {
chat.cached_summary = cachedSummary;
chat.cached_summary_at = cachedSummaryAt;
}
},
[types.ASSIGN_PRIORITY](_state, { priority, conversationId }) {
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
chat.priority = priority;
@@ -51,6 +51,7 @@ export default {
UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES:
'UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES',
UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY',
UPDATE_CONVERSATION_CACHED_SUMMARY: 'UPDATE_CONVERSATION_CACHED_SUMMARY',
UPDATE_CONVERSATION_CALL_STATUS: 'UPDATE_CONVERSATION_CALL_STATUS',
UPDATE_MESSAGE_CALL_STATUS: 'UPDATE_MESSAGE_CALL_STATUS',
SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES',