diff --git a/backend/open_webui/migrations/versions/959eaac8f909_add_automation_folder_id.py b/backend/open_webui/migrations/versions/959eaac8f909_add_automation_folder_id.py new file mode 100644 index 0000000000..3eafd1a246 --- /dev/null +++ b/backend/open_webui/migrations/versions/959eaac8f909_add_automation_folder_id.py @@ -0,0 +1,53 @@ +"""add automation folder id + +Revision ID: 959eaac8f909 +Revises: 55f1302ac17c +Create Date: 2026-07-26 19:19:31.345756 + +""" +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import context, op + +# revision identifiers, used by Alembic. +revision: str = '959eaac8f909' +down_revision: str | None = '55f1302ac17c' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + if context.is_offline_mode(): + op.add_column('automation', sa.Column('folder_id', sa.Text(), nullable=True)) + op.create_index('ix_automation_user_folder', 'automation', ['user_id', 'folder_id']) + return + + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = {col['name'] for col in inspector.get_columns('automation')} + indexes = {index['name'] for index in inspector.get_indexes('automation')} + + if 'folder_id' not in columns: + op.add_column('automation', sa.Column('folder_id', sa.Text(), nullable=True)) + + if 'ix_automation_user_folder' not in indexes: + op.create_index('ix_automation_user_folder', 'automation', ['user_id', 'folder_id']) + + +def downgrade() -> None: + if context.is_offline_mode(): + op.drop_index('ix_automation_user_folder', table_name='automation') + op.drop_column('automation', 'folder_id') + return + + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = {col['name'] for col in inspector.get_columns('automation')} + indexes = {index['name'] for index in inspector.get_indexes('automation')} + + if 'ix_automation_user_folder' in indexes: + op.drop_index('ix_automation_user_folder', table_name='automation') + + if 'folder_id' in columns: + op.drop_column('automation', 'folder_id') diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py index fbd74ccf5e..c0a7416cc6 100644 --- a/backend/open_webui/models/automations.py +++ b/backend/open_webui/models/automations.py @@ -21,6 +21,7 @@ class Automation(Base): id = Column(Text, primary_key=True) user_id = Column(Text, nullable=False) + folder_id = Column(Text, nullable=True) name = Column(Text, nullable=False) data = Column(JSON, nullable=False) # {prompt, model_id, rrule} meta = Column(JSON, nullable=True) @@ -31,7 +32,10 @@ class Automation(Base): created_at = Column(BigInteger, nullable=False) updated_at = Column(BigInteger, nullable=False) - __table_args__ = (Index('ix_automation_next_run', 'next_run_at'),) + __table_args__ = ( + Index('ix_automation_next_run', 'next_run_at'), + Index('ix_automation_user_folder', 'user_id', 'folder_id'), + ) class AutomationRun(Base): @@ -72,6 +76,7 @@ class AutomationModel(BaseModel): id: str user_id: str + folder_id: Optional[str] = None name: str data: dict meta: Optional[dict] = None @@ -96,6 +101,7 @@ class AutomationRunModel(BaseModel): class AutomationForm(BaseModel): name: str + folder_id: Optional[str] = None data: AutomationData meta: Optional[dict] = None is_active: Optional[bool] = True @@ -129,6 +135,7 @@ class AutomationTable: row = Automation( id=str(uuid4()), user_id=user_id, + folder_id=form.folder_id, name=form.name, data=form.data.model_dump(), meta=form.meta, @@ -164,6 +171,7 @@ class AutomationTable: user_id: str, query: Optional[str] = None, status: Optional[str] = None, + folder_id: Optional[str] = None, skip: int = 0, limit: int = 30, db: Optional[AsyncSession] = None, @@ -171,6 +179,9 @@ class AutomationTable: async with get_async_db_context(db) as db: stmt = select(Automation).filter_by(user_id=user_id) + if folder_id is not None: + stmt = stmt.filter(Automation.folder_id == (folder_id or None)) + if query: search = f'%{query}%' # Search in name and prompt inside JSON data @@ -216,6 +227,7 @@ class AutomationTable: if not row: return None row.name = form.name + row.folder_id = form.folder_id row.data = form.data.model_dump() row.meta = form.meta if form.is_active is not None: @@ -225,6 +237,23 @@ class AutomationTable: await db.commit() return AutomationModel.model_validate(row) + async def clear_folder_ids( + self, + user_id: str, + folder_ids: list[str], + db: Optional[AsyncSession] = None, + ) -> int: + if not folder_ids: + return 0 + async with get_async_db_context(db) as db: + result = await db.execute( + update(Automation) + .where(Automation.user_id == user_id, Automation.folder_id.in_(folder_ids)) + .values(folder_id=None, updated_at=int(time.time_ns())) + ) + await db.commit() + return result.rowcount or 0 + async def toggle( self, id: str, diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 2162755b49..cd73c99142 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -27,6 +27,7 @@ from sqlalchemy import ( UniqueConstraint, and_, delete, + exists, func, or_, select, @@ -1435,6 +1436,37 @@ class ChatTable: except Exception: return None + async def count_unread_by_folder_ids( + self, + user_id: str, + folder_ids: list[str], + db: AsyncSession | None = None, + ) -> dict[str, int]: + if not folder_ids: + return {} + + unfinished_assistant = ( + select(ChatMessage.id) + .where(ChatMessage.chat_id == Chat.id) + .where(ChatMessage.role == 'assistant') + .where(ChatMessage.done.is_(False)) + .exists() + ) + + async with get_async_db_context(db) as session: + result = await session.execute( + select(Chat.folder_id, func.count(Chat.id)) + .where( + Chat.user_id == user_id, + Chat.folder_id.in_(folder_ids), + Chat.archived == False, + Chat.updated_at > func.coalesce(Chat.last_read_at, 0), + ~unfinished_assistant, + ) + .group_by(Chat.folder_id) + ) + return {folder_id: count for folder_id, count in result.all() if folder_id} + async def get_chats(self, skip: int = 0, limit: int = 50, db: AsyncSession | None = None) -> list[ChatModel]: async with get_async_db_context(db) as session: stmt = select(Chat).where(Chat.meta['internal'].as_boolean().is_not(True)) diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index a06a5c51f8..11d5dd5427 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -58,6 +58,7 @@ class FolderNameIdResponse(BaseModel): meta: Optional[FolderMetadataResponse] = None parent_id: Optional[str] = None is_expanded: bool = False + unread_count: int = 0 created_at: int updated_at: int diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py index f7d027bad9..e01c5c255b 100644 --- a/backend/open_webui/routers/automations.py +++ b/backend/open_webui/routers/automations.py @@ -16,6 +16,7 @@ from open_webui.models.automations import ( Automations, ) from open_webui.models.config import Config +from open_webui.models.folders import Folders from open_webui.utils.access_control import has_permission from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.automations import ( @@ -56,16 +57,11 @@ async def check_automations_permission(request, user): def check_automation_access(automation, user): - if not automation: + if not automation or user.id != automation.user_id: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, ) - if user.role != 'admin' and user.id != automation.user_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.UNAUTHORIZED, - ) async def check_automation_limits(request, user, rrule_str: str, db, is_create: bool = False): @@ -97,6 +93,17 @@ async def check_automation_limits(request, user, rrule_str: str, db, is_create: ) +async def check_automation_folder_access(folder_id: Optional[str], user, db: AsyncSession): + if folder_id is None: + return + folder = await Folders.get_folder_by_id_and_user_id(folder_id, user.id, db=db) + if not folder: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + async def enrich_automation(automation: AutomationModel, db: AsyncSession, tz: str = None) -> AutomationResponse: """Full enrichment for single-item views (includes next_runs computation).""" last_run = await AutomationRuns.get_latest(automation.id, db=db) @@ -117,6 +124,7 @@ async def get_automation_items( request: Request, query: Optional[str] = None, status: Optional[str] = None, + folder_id: Optional[str] = None, page: Optional[int] = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), @@ -130,6 +138,7 @@ async def get_automation_items( user_id=user.id, query=query, status=status, + folder_id=folder_id, skip=skip, limit=limit, db=db, @@ -164,6 +173,7 @@ async def create_new_automation( db: AsyncSession = Depends(get_async_session), ): await check_automations_permission(request, user) + await check_automation_folder_access(form_data.folder_id, user, db) try: validate_rrule(form_data.data.rrule, tz=user.timezone) except ValueError as e: @@ -182,7 +192,7 @@ async def create_new_automation( EVENTS.AUTOMATION_CREATED, actor=user, subject_id=automation.id, - data={'name': automation.name, 'is_active': automation.is_active}, + data={'name': automation.name, 'is_active': automation.is_active, 'folder_id': automation.folder_id}, ) return response @@ -221,6 +231,7 @@ async def update_automation_by_id( await check_automations_permission(request, user) automation = await Automations.get_by_id(id, db=db) check_automation_access(automation, user) + await check_automation_folder_access(form_data.folder_id, user, db) try: validate_rrule(form_data.data.rrule, tz=user.timezone) @@ -240,7 +251,7 @@ async def update_automation_by_id( EVENTS.AUTOMATION_UPDATED, actor=user, subject_id=updated.id, - data={'name': updated.name, 'is_active': updated.is_active}, + data={'name': updated.name, 'is_active': updated.is_active, 'folder_id': updated.folder_id}, ) return response diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index 8e85eccc27..cfce4ad996 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -12,6 +12,7 @@ from open_webui.config import UPLOAD_DIR from open_webui.constants import ERROR_MESSAGES from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session +from open_webui.models.chat_messages import ChatMessages from open_webui.models.config import Config from open_webui.models.chats import Chats from open_webui.models.folders import ( @@ -22,6 +23,7 @@ from open_webui.models.folders import ( FolderUpdateForm, ) from open_webui.models.access_grants import AccessGrants +from open_webui.models.automations import Automations from open_webui.models.groups import Groups from open_webui.models.users import Users from open_webui.utils.access_control import has_permission @@ -30,6 +32,7 @@ from open_webui.utils.access_control import ( ) from open_webui.utils.access_control.files import get_accessible_folder_files from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.tasks import has_active_tasks from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession @@ -92,9 +95,26 @@ async def get_folders( folder.id, user.id, FolderUpdateForm(data=folder.data), db=db ) - folder_list.append(FolderNameIdResponse(**folder.model_dump())) + folder_list.append(folder) - return folder_list + direct_unread_counts = await Chats.count_unread_by_folder_ids( + user.id, [folder.id for folder in folder_list], db=db + ) + parent_by_id = {folder.id: folder.parent_id for folder in folder_list} + unread_counts = dict.fromkeys(parent_by_id.keys(), 0) + for unread_folder_id, unread_count in direct_unread_counts.items(): + current_id = unread_folder_id + seen = set() + while current_id and current_id not in seen: + seen.add(current_id) + if current_id in unread_counts: + unread_counts[current_id] += unread_count + current_id = parent_by_id.get(current_id) + + return [ + FolderNameIdResponse(**folder.model_dump(), unread_count=unread_counts.get(folder.id, 0)) + for folder in folder_list + ] ############################ @@ -529,6 +549,9 @@ async def get_shared_folder_chats( u = await Users.get_user_by_id(uid, db=db) owner_cache[uid] = u.name if u else 'Unknown' chat['owner_name'] = owner_cache[uid] + chat['active'] = False + if await has_active_tasks(request.app.state.redis, chat['id']): + chat['active'] = await ChatMessages.has_unfinished_assistant_by_chat_id(chat['id'], db=db) response = { 'chats': [{**chat, 'readonly': chat['user_id'] != user.id} for chat in chats], @@ -607,6 +630,8 @@ async def delete_folder_by_id( # Clean up access grants for this folder await AccessGrants.revoke_all_access('folder', folder_id, db=db) + await Automations.clear_folder_ids(folder_owner_id, folder_ids, db=db) + await publish_event( request, EVENTS.FOLDER_DELETED, diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index e9ebb4d770..c16ee91566 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -535,6 +535,14 @@ async def chat_events(sid, data): if event_type == 'last_read_at': if not await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id']): return + await sio.emit( + 'events', + { + 'chat_id': data['chat_id'], + 'data': {'type': 'chat:list'}, + }, + room=f'user:{user["id"]}', + ) try: from open_webui.utils.timers import cancel_timers_for_chat diff --git a/backend/open_webui/tools/builtin.py b/backend/open_webui/tools/builtin.py index cd136d947f..bf4ab51f71 100644 --- a/backend/open_webui/tools/builtin.py +++ b/backend/open_webui/tools/builtin.py @@ -3255,10 +3255,22 @@ async def update_task( # ============================================================================= +async def _validate_owned_automation_folder(user_id: str, folder_id: Optional[str]) -> Optional[str]: + if not folder_id: + return None + from open_webui.models.folders import Folders + + folder = await Folders.get_folder_by_id_and_user_id(folder_id, user_id) + if not folder: + raise ValueError('Folder not found') + return folder.id + + async def create_automation( name: str, prompt: str, rrule: str, + folder_id: Optional[str] = None, __request__: Request = None, __user__: dict = None, __metadata__: dict = None, @@ -3281,6 +3293,7 @@ async def create_automation( :param name: A short descriptive name for the automation :param prompt: The prompt/instructions to execute on each run :param rrule: An iCalendar RRULE string defining the schedule + :param folder_id: Optional owner-owned folder ID for generated chats :return: JSON with the created automation details including id, next scheduled runs """ if __request__ is None: @@ -3308,6 +3321,11 @@ async def create_automation( if not model_id: return json.dumps({'error': 'Could not detect current model'}) + try: + folder_id = await _validate_owned_automation_folder(user_id, folder_id) + except ValueError as e: + return json.dumps({'error': str(e)}) + # Validate the RRULE try: validate_rrule(rrule, tz=user.timezone) @@ -3322,6 +3340,7 @@ async def create_automation( tz = user.timezone form = AutomationForm( name=name, + folder_id=folder_id, data=AutomationData( prompt=prompt, model_id=model_id, @@ -3337,6 +3356,7 @@ async def create_automation( 'status': 'success', 'id': automation.id, 'name': automation.name, + 'folder_id': automation.folder_id, 'model_id': model_id, 'is_active': automation.is_active, 'next_runs': next_n_runs_ns(rrule, tz=tz), @@ -3354,6 +3374,7 @@ async def update_automation( prompt: Optional[str] = None, rrule: Optional[str] = None, model_id: Optional[str] = None, + folder_id: Optional[str] = None, __request__: Request = None, __user__: dict = None, ) -> str: @@ -3365,6 +3386,7 @@ async def update_automation( :param prompt: New prompt/instructions (optional) :param rrule: New iCalendar RRULE schedule string (optional). See create_automation for format examples. :param model_id: New model ID to use (optional) + :param folder_id: New owner-owned folder ID (optional); pass an empty string to clear :return: JSON with the updated automation details """ if __request__ is None: @@ -3395,6 +3417,13 @@ async def update_automation( new_prompt = prompt if prompt is not None else automation.data.get('prompt', '') new_model_id = model_id if model_id is not None else automation.data.get('model_id', '') new_rrule = rrule if rrule is not None else automation.data.get('rrule', '') + if folder_id is None: + new_folder_id = automation.folder_id + else: + try: + new_folder_id = await _validate_owned_automation_folder(user_id, folder_id) + except ValueError as e: + return json.dumps({'error': str(e)}) # Validate RRULE if changed if rrule is not None: @@ -3411,6 +3440,7 @@ async def update_automation( tz = user.timezone form = AutomationForm( name=new_name, + folder_id=new_folder_id, data=AutomationData( prompt=new_prompt, model_id=new_model_id, @@ -3426,6 +3456,7 @@ async def update_automation( 'status': 'success', 'id': updated.id, 'name': updated.name, + 'folder_id': updated.folder_id, 'model_id': new_model_id, 'is_active': updated.is_active, 'next_runs': next_n_runs_ns(new_rrule, tz=tz), @@ -3439,6 +3470,7 @@ async def update_automation( async def list_automations( status: Optional[str] = None, + folder_id: Optional[str] = None, count: int = 10, __request__: Request = None, __user__: dict = None, @@ -3447,6 +3479,7 @@ async def list_automations( List the user's scheduled automations. :param status: Filter by status: "active", "paused", or omit for all + :param folder_id: Optional owner-owned folder ID filter; pass an empty string to clear the folder filter :param count: Maximum number of automations to return (default: 10) :return: JSON list of automations with id, name, prompt snippet, schedule, status, and next runs """ @@ -3463,10 +3496,16 @@ async def list_automations( user_id = __user__.get('id') user = await Users.get_user_by_id(user_id) + if folder_id: + try: + folder_id = await _validate_owned_automation_folder(user_id, folder_id) + except ValueError as e: + return json.dumps({'error': str(e)}) result = await Automations.search_automations( user_id=user_id, status=status, + folder_id=folder_id, skip=0, limit=count, ) @@ -3481,6 +3520,7 @@ async def list_automations( { 'id': item.id, 'name': item.name, + 'folder_id': item.folder_id, 'prompt_snippet': snippet, 'model_id': item.data.get('model_id', ''), 'rrule': rrule, diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index 3d317585ea..9ce8084358 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -34,6 +34,7 @@ from open_webui.internal.db import get_async_db from open_webui.models.automations import AutomationModel, AutomationRuns, Automations from open_webui.models.chats import ChatForm, Chats from open_webui.models.config import Config +from open_webui.models.folders import Folders from open_webui.models.users import Users from open_webui.utils.auth import create_token from open_webui.utils.misc import parse_duration @@ -430,7 +431,10 @@ async def execute_automation(app, automation: AutomationModel) -> None: prompt = await prompt_template(automation.data['prompt'], user) model_id = automation.data['model_id'] - terminal_config = automation.data.get('terminal') + folder_id = automation.folder_id + if folder_id and not await Folders.get_folder_by_id_and_user_id(folder_id, automation.user_id): + await Automations.clear_folder_ids(automation.user_id, [folder_id]) + folder_id = None # Generate proper UUIDs for messages (same as frontend) user_msg_id = str(uuid4()) @@ -441,6 +445,7 @@ async def execute_automation(app, automation: AutomationModel) -> None: chat_id, automation.user_id, ChatForm( + folder_id=folder_id, chat={ 'title': automation.name, 'models': [model_id], diff --git a/src/lib/apis/automations/index.ts b/src/lib/apis/automations/index.ts index a79fe1ddc8..593a56cab1 100644 --- a/src/lib/apis/automations/index.ts +++ b/src/lib/apis/automations/index.ts @@ -14,6 +14,7 @@ export type AutomationData = { export type AutomationForm = { name: string; + folder_id?: string | null; data: AutomationData; meta?: { system_prompt?: string; @@ -36,6 +37,7 @@ export type AutomationRunModel = { export type AutomationResponse = { id: string; user_id: string; + folder_id: string | null; name: string; data: AutomationData; meta: Record | null; @@ -53,7 +55,8 @@ export const getAutomationItems = async ( token: string, query: string | null, status: string | null, - page: number + page: number, + folder_id?: string | null ): Promise<{ items: AutomationResponse[]; total: number }> => { let error = null; @@ -67,6 +70,9 @@ export const getAutomationItems = async ( if (page) { searchParams.append('page', page.toString()); } + if (folder_id !== undefined && folder_id !== null) { + searchParams.append('folder_id', folder_id); + } const res = await fetch(`${WEBUI_API_BASE_URL}/automations/list?${searchParams.toString()}`, { method: 'GET', diff --git a/src/lib/components/AutomationModal.svelte b/src/lib/components/AutomationModal.svelte index baa742fdea..23028b3bc8 100644 --- a/src/lib/components/AutomationModal.svelte +++ b/src/lib/components/AutomationModal.svelte @@ -8,6 +8,9 @@ import ScheduleDropdown from '$lib/components/automations/ScheduleDropdown.svelte'; import ModelDropdown from '$lib/components/automations/ModelDropdown.svelte'; + import FolderDropdown from '$lib/components/automations/FolderDropdown.svelte'; + import { getFolders } from '$lib/apis/folders'; + import { folders } from '$lib/stores'; import { createAutomation, @@ -26,9 +29,11 @@ let name = ''; let prompt = ''; let model_id = ''; + let folder_id = ''; let is_active = true; let loading = false; + let foldersLoaded = false; // Schedule dropdown ref let scheduleDropdown: ScheduleDropdown; @@ -49,6 +54,7 @@ try { const form: AutomationForm = { name: name.trim(), + folder_id: folder_id || null, data: { prompt: prompt.trim(), model_id: model_id.trim(), @@ -77,11 +83,17 @@ const init = async () => { await tick(); + if (!foldersLoaded && ($folders ?? []).length === 0) { + const res = await getFolders(localStorage.token).catch(() => null); + if (res) folders.set(res); + foldersLoaded = true; + } if (automation) { name = automation.name; prompt = automation.data.prompt; model_id = automation.data.model_id; + folder_id = automation.folder_id ?? ''; is_active = automation.is_active; if (scheduleDropdown) { scheduleDropdown.parseRrule(automation.data.rrule); @@ -90,6 +102,9 @@ name = cloneFrom.name; prompt = cloneFrom.data.prompt; model_id = cloneFrom.data.model_id; + folder_id = ($folders ?? []).some((folder) => folder.id === cloneFrom.folder_id) + ? (cloneFrom.folder_id ?? '') + : ''; is_active = true; if (scheduleDropdown) { scheduleDropdown.parseRrule(cloneFrom.data.rrule); @@ -98,6 +113,7 @@ name = ''; prompt = ''; model_id = ''; + folder_id = ''; is_active = true; } }; @@ -145,6 +161,8 @@ + +
diff --git a/src/lib/components/automations/AutomationEditor.svelte b/src/lib/components/automations/AutomationEditor.svelte index 32d3a81d9f..4d00c04585 100644 --- a/src/lib/components/automations/AutomationEditor.svelte +++ b/src/lib/components/automations/AutomationEditor.svelte @@ -7,7 +7,8 @@ import localizedFormat from 'dayjs/plugin/localizedFormat'; import type i18nType from '$lib/i18n'; - import { WEBUI_NAME } from '$lib/stores'; + import { WEBUI_NAME, folders } from '$lib/stores'; + import { getFolders } from '$lib/apis/folders'; import { getAutomationById, @@ -42,6 +43,19 @@ let runsLoading = false; let hasMoreRuns = true; let runsPage = 0; + let foldersLoaded = false; + + const ensureFolders = async () => { + if (foldersLoaded || ($folders ?? []).length > 0) return; + const res = await getFolders(localStorage.token).catch(() => null); + if (res) folders.set(res); + foldersLoaded = true; + }; + + const getFolderName = (folderId: string | null): string => + folderId + ? (($folders ?? []).find((folder) => folder.id === folderId)?.name ?? $i18n.t('None')) + : $i18n.t('None'); const formatTime = (ts: number | null): string => { if (!ts) return '-'; @@ -201,6 +215,7 @@ onMount(async () => { is_active = automation.is_active; + await ensureFolders(); await loadRuns(); }); @@ -263,6 +278,15 @@
+
+ + {$i18n.t('Folder')} + + + {getFolderName(automation.folder_id)} + +
+
{$i18n.t('Model')} diff --git a/src/lib/components/automations/FolderDropdown.svelte b/src/lib/components/automations/FolderDropdown.svelte new file mode 100644 index 0000000000..dfd6421c3f --- /dev/null +++ b/src/lib/components/automations/FolderDropdown.svelte @@ -0,0 +1,147 @@ + + + +
+ + {#if folderOptions.length > 0} +
+
+ {$i18n.t('Folders')} +
+ {/if} + + {#each filteredFolderOptions as folder (folder.id)} + {@const path = folderPath(folder)} + + {:else} +
+ {folderOptions.length > 0 ? $i18n.t('No results found') : $i18n.t('No folders')} +
+ {/each} + + diff --git a/src/lib/components/chat/Placeholder/ChatList.svelte b/src/lib/components/chat/Placeholder/ChatList.svelte index 77bd5ae4ab..23a99c2cd3 100644 --- a/src/lib/components/chat/Placeholder/ChatList.svelte +++ b/src/lib/components/chat/Placeholder/ChatList.svelte @@ -12,6 +12,8 @@ import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte'; import ChevronRight from '$lib/components/icons/ChevronRight.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import { chatId } from '$lib/stores'; dayjs.extend(localizedFormat); @@ -138,6 +140,11 @@ {/if} {#each chatList as chat, idx (chat.id)} + {@const unread = + chat.id !== $chatId && + !chat.active && + (chat.last_read_at == null || + (chat.updated_at != null && chat.updated_at > chat.last_read_at))} {#if (idx === 0 || (idx > 0 && chat.time_range !== chatList[idx - 1].time_range)) && chat?.time_range}
-
- {chat?.title} +
+ {#if chat.active} +
+ +
+ {:else if unread} +
+
+
+ {/if} + +
+ {chat?.title} +