Skip to content

Commit

Permalink
Ensure idempotency when generating audiocast (#18)
Browse files Browse the repository at this point in the history
* improved cleanup and abstraction

* remove session_id key prop from sidebar to fix issue with scrolling to top

* add cleanup for generated audio files; ensure temporary files are removed after processing

* install alert-dialog component

* wip: use a sidebar_item_action

* refine sidebar item and item action

* create sessionItems$ store value

* fix issue with sidebar state when audiocast is added or removed

* implement delete audiocast session on the server
  • Loading branch information
nwaughachukwuma authored Nov 27, 2024
1 parent f9a3b91 commit 68b1c47
Show file tree
Hide file tree
Showing 30 changed files with 398 additions and 92 deletions.
22 changes: 11 additions & 11 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
anthropic

asyncio
elevenlabs
fastapi
uvicorn
gunicorn
uvloop
fastapi-utilities
ffmpeg-python

firebase-admin
google-api-python-client
google-auth
google-cloud-storage
google-generativeai
gunicorn

asyncio
httpx
redis[hiredis]

anthropic
elevenlabs
openai

pycairo
pydantic
pydub
pypdf[crypto]
python-dotenv
ffmpeg-python
python-multipart
python-slugify

ruff
seewav
setuptools
uvicorn
uvloop
redis[hiredis]

async-web-search

lxml
beautifulsoup4
pypdf[crypto]
async-web-search
9 changes: 9 additions & 0 deletions api/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,12 @@ async def save_uploaded_files_endpoint(files: list[UploadFile], sessionId: str =
"""
result = await UploadedFiles(session_id=sessionId)._save_sources(files)
return result


@app.delete("/delete-session/{sessionId}")
def delete_session_endpoint(sessionId: str):
"""
Delete audiocast session
"""
SessionManager._delete_session(sessionId)
return "Deleted"
5 changes: 5 additions & 0 deletions api/src/utils/generate_audiocast.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from fastapi import BackgroundTasks, HTTPException

from src.services.storage import StorageManager
Expand Down Expand Up @@ -36,6 +38,9 @@ def post_generate_audio(
waveform_utils.run_all()
except Exception as e:
print(f"Error in generate_audiocast background_tasks: {str(e)}")
finally:
if os.path.exists(audio_path):
os.remove(audio_path)


async def generate_audiocast(request: GenerateAudioCastRequest, background_tasks: BackgroundTasks):
Expand Down
4 changes: 4 additions & 0 deletions api/src/utils/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,7 @@ def on_snapshot(doc_snapshot, _changes, _read_time):
callback(info)

return doc_ref.on_snapshot(on_snapshot)

@staticmethod
def _delete_session(doc_id: str):
return DBManager()._delete_document(collections["audiora_sessions"], doc_id)
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
"test": "npm run test:integration && npm run test:unit"
},
"dependencies": {
"bits-ui": "^0.21.16",
"canvas-confetti": "^1.9.3",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
Expand Down Expand Up @@ -55,6 +54,7 @@
"@types/ramda": "^0.30.1",
"@types/throttle-debounce": "^5.0.2",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
Expand Down
10 changes: 8 additions & 2 deletions app/src/lib/components/ChatContainer.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<script>
import { getSessionContext } from '@/stores/sessionContext.svelte';
import ChatBoxContainer from './ChatBoxContainer.svelte';
import { isfinalResponse } from '@/utils/session.utils';
export let searchTerm = '';
export let disableTextInput = false;
const { sessionCompleted$, fetchingSource$, audioSource$ } = getSessionContext();
const { sessionCompleted$, fetchingSource$, audioSource$, session$ } = getSessionContext();
let navLoading = false;
$: chats = $session$?.chats || [];
$: hasFinalResponse = chats.some(isfinalResponse);
</script>

<div
Expand All @@ -18,7 +24,7 @@
<div class="h-24"></div>
</div>

{#if !$sessionCompleted$ && !$fetchingSource$ && !$audioSource$}
{#if !hasFinalResponse && !$sessionCompleted$ && !$fetchingSource$ && !$audioSource$}
<ChatBoxContainer
bind:searchTerm
loading={navLoading}
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/components/ExampleCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
const { addChatItem, startSession } = getSessionContext();
function handleClick() {
async function handleClick() {
startSession(category);
addChatItem({ id: uuid(), content, role: 'user', loading: false });
goto(href);
return goto(href, { invalidateAll: true, replaceState: true });
}
</script>

Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/components/ManageAudioSourceDrawer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@

<Drawer.Root {snapPoints} bind:activeSnapPoint direction="bottom" dismissible shouldScaleBackground>
<Drawer.Trigger>
<Button variant="ghost" class="bg-gray-800 text-base py-6 w-full hover:bg-gray-700 text-white"
>Manage Audiocast Sources
<Button variant="ghost" class="bg-gray-800 text-base py-6 w-full hover:bg-gray-700 text-white">
Manage Sources
</Button>
</Drawer.Trigger>

Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/components/RenderExamples.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
{#each contentExamplesDict as [category, content]}
{@const href = `/chat/${sessionId}?category=${category}`}
{@const href = `/chat/${sessionId}?category=${category}&chat`}
<ExampleCard {content} {href} {category} />
{/each}
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/components/RootNav.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import Logo from './Logo.svelte';
import SearchSlideSheet from './SlideSheet.svelte';
import SearchSidebar from './Sidebar.svelte';
import Sidebar from './Sidebar.svelte';
import { Button } from './ui/button';
import { getAppContext } from '$lib/stores/appContext.svelte';
import { invalidateAll } from '$app/navigation';
Expand All @@ -22,7 +22,7 @@
<svelte-fragment>
{#if $openSettingsDrawer$}
{#key sessionId}
<SearchSidebar on:clickItem={() => ($openSettingsDrawer$ = false)} />
<Sidebar on:clickItem={() => ($openSettingsDrawer$ = false)} />
{/key}
{/if}
</svelte-fragment>
Expand Down
106 changes: 69 additions & 37 deletions app/src/lib/components/Sidebar.svelte
Original file line number Diff line number Diff line change
@@ -1,58 +1,78 @@
<script context="module">
<script lang="ts" context="module">
import { SESSION_KEY } from '@/stores/sessionContext.svelte';
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const last24Hrs = Date.now() - ONE_DAY_MS;
const last7Days = Date.now() - 4 * ONE_DAY_MS;
export function getSessionItems() {
return Object.entries(localStorage)
.filter(([key]) => key.startsWith(SESSION_KEY))
.map(
([key, value]) =>
[key.replace(`${SESSION_KEY}_`, ''), JSON.parse(value) as Session] as const
)
.filter(([_, v]) => Boolean(v));
}
function getSidebarItems(sessionItems: (readonly [string, Session])[]) {
return sessionItems
.filter(([_, item]) => item.chats.length)
.map(([sessionId, session]) => ({
sessionId,
title: session.title || 'Untitled',
nonce: session.nonce,
category: session.category,
completed: session.completed
}))
.sort((a, b) => b.nonce - a.nonce);
}
</script>

<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { browser } from '$app/environment';
import { type Session, SESSION_KEY, getSessionContext } from '../stores/sessionContext.svelte';
import SearchSidebarItem from './SidebarItem.svelte';
import { getSessionContext, type Session } from '../stores/sessionContext.svelte';
import SidebarItem from './SidebarItem.svelte';
import { getAppContext } from '../stores/appContext.svelte';
import HeadphoneOff from 'lucide-svelte/icons/headphone-off';
import cs from 'clsx';
import { page } from '$app/stores';
import NewAudiocastButton from './NewAudiocastButton.svelte';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { env } from '@env';
const dispatch = createEventDispatcher<{ clickItem: void }>();
const { openSettingsDrawer$ } = getAppContext();
$: ({ session$ } = getSessionContext());
const { session$, refreshSidebar$ } = getSessionContext();
$: sessionItems = browser || $session$ ? getSessionItems() : [];
$: sidebarItems = getSidebarItems(sessionItems);
$: sidebarItems = sessionItems
.filter(([_, item]) => item.chats.length)
.map(([sessionId, session]) => ({
sessionId,
title: session.title || 'Untitled',
nonce: session.nonce,
category: session.category,
completed: session.completed
}))
.sort((a, b) => b.nonce - a.nonce);
$: if ($refreshSidebar$) sidebarItems = getSidebarItems(getSessionItems());
$: inLast24Hrs = sidebarItems.filter((i) => i.nonce > last24Hrs);
$: inLast7Days = sidebarItems.filter((i) => i.nonce < last24Hrs && i.nonce > last7Days);
$: inLast30Days = sidebarItems.filter((i) => i.nonce < last24Hrs && i.nonce < last7Days);
function getSessionItems() {
return Object.entries(localStorage)
.filter(([key]) => key.startsWith(SESSION_KEY))
.map(
([key, value]) =>
[key.replace(`${SESSION_KEY}_`, ''), JSON.parse(value) as Session] as const
)
.filter(([_, v]) => Boolean(v));
}
function dispatchClickItem() {
dispatch('clickItem');
}
$: rootPath = $page.url.pathname === '/';
function deleteSession(sessionId: string) {
return async () => {
localStorage.removeItem(`${SESSION_KEY}_${sessionId}`);
sidebarItems = getSidebarItems(getSessionItems());
void fetch(`${env.API_BASE_URL}/delete-session/${sessionId}`, {
method: 'DELETE'
}).catch(() => void 0);
return goto('/', { invalidateAll: true, replaceState: true });
};
}
</script>

<div
Expand All @@ -63,7 +83,7 @@
style="transition: width 0.3s cubic-bezier(0.34, 1.47, 0.64, 1), padding 0.3s ease;"
>
<nav
class={cs('flex w-full flex-col gap-x-2 lg:gap-x-0 lg:gap-y-1', {
class={cs('flex w-full flex-col gap-x-2 pt-2 lg:gap-x-0 lg:gap-y-1', {
'opacity-100': $openSettingsDrawer$,
'opacity-0': !$openSettingsDrawer$
})}
Expand All @@ -84,23 +104,35 @@
{/if}

<div class="flex w-full flex-col gap-y-1.5 pt-2" class:hidden={!inLast24Hrs.length}>
<div class="px-2 text-sm font-medium">Today</div>
{#each inLast24Hrs as item}
<SearchSidebarItem {item} on:click={dispatchClickItem} />
<div class="px-2 text-sm font-semibold">Today</div>
{#each inLast24Hrs as item (item.sessionId)}
<SidebarItem
{item}
on:click={dispatchClickItem}
on:deleteSession={deleteSession(item.sessionId)}
/>
{/each}
</div>

<div class="flex w-full flex-col gap-y-1.5 pt-6" class:hidden={!inLast7Days.length}>
<div class="px-2 text-sm font-medium">Last 7 days</div>
{#each inLast7Days as item}
<SearchSidebarItem {item} on:click={dispatchClickItem} />
<div class="px-2 text-sm font-semibold">Last 7 days</div>
{#each inLast7Days as item (item.sessionId)}
<SidebarItem
{item}
on:click={dispatchClickItem}
on:deleteSession={deleteSession(item.sessionId)}
/>
{/each}
</div>

<div class="flex w-full flex-col gap-y-1.5 pt-6" class:hidden={!inLast30Days.length}>
<div class="px-2 text-sm font-medium">Last month</div>
{#each inLast30Days as item}
<SearchSidebarItem {item} on:click={dispatchClickItem} />
<div class="px-2 text-sm font-semibold">Last month</div>
{#each inLast30Days as item (item.sessionId)}
<SidebarItem
{item}
on:click={dispatchClickItem}
on:deleteSession={deleteSession(item.sessionId)}
/>
{/each}
</div>
<div class="block h-20"></div>
Expand Down
16 changes: 8 additions & 8 deletions app/src/lib/components/SidebarItem.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" context="module">
import type { ContentCategory } from '@/utils/types';
export type SearchSidebarItem = {
export type SidebarItemModel = {
title: string;
nonce: number;
sessionId: string;
Expand All @@ -15,13 +15,11 @@
import { page } from '$app/stores';
import { Button } from './ui/button';
import { cn } from '@/utils/ui.utils';
import SidebarItemActions from './SidebarItemActions.svelte';
export let item: SearchSidebarItem;
export let item: SidebarItemModel;
const [send, receive] = crossfade({
duration: 250,
easing: cubicInOut
});
const [send, receive] = crossfade({ duration: 250, easing: cubicInOut });
$: href = item.completed
? `/audiocast/${item.sessionId}`
Expand All @@ -32,7 +30,7 @@
<Button
{href}
variant="ghost"
class="relative no-underline group hover:no-underline justify-start py-6 flex items-center px-2 hover:bg-gray-700 bg-gray-900"
class="relative no-underline group justify-between hover:no-underline py-6 flex items-center px-2 hover:bg-gray-700 bg-gray-900"
data-sveltekit-noscroll
on:click
>
Expand All @@ -52,7 +50,9 @@
{item.title}
</div>

<div class="text-[10px] px-2 hidden group-hover:block absolute bottom-0 right-0 text-sky-200">
<div class="text-[10px] px-2 hidden group-hover:block absolute bottom-0 left-0 text-sky-200">
{item.category}
</div>

<SidebarItemActions on:deleteSession />
</Button>
Loading

0 comments on commit 68b1c47

Please sign in to comment.