Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6750d6c238 | |||
| fbecefae25 |
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn
|
|||||||
|
|
||||||
from astrbot.core.db.po import (
|
from astrbot.core.db.po import (
|
||||||
Attachment,
|
Attachment,
|
||||||
|
ChatUIProject,
|
||||||
CommandConfig,
|
CommandConfig,
|
||||||
CommandConflict,
|
CommandConflict,
|
||||||
ConversationV2,
|
ConversationV2,
|
||||||
@@ -17,6 +18,7 @@ from astrbot.core.db.po import (
|
|||||||
PlatformSession,
|
PlatformSession,
|
||||||
PlatformStat,
|
PlatformStat,
|
||||||
Preference,
|
Preference,
|
||||||
|
SessionProjectRelation,
|
||||||
Stats,
|
Stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -446,8 +448,11 @@ class BaseDatabase(abc.ABC):
|
|||||||
platform_id: str | None = None,
|
platform_id: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
) -> list[PlatformSession]:
|
) -> list[dict]:
|
||||||
"""Get all Platform sessions for a specific creator (username) and optionally platform."""
|
"""Get all Platform sessions for a specific creator (username) and optionally platform.
|
||||||
|
|
||||||
|
Returns a list of dicts containing session info and project info (if session belongs to a project).
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@@ -463,3 +468,80 @@ class BaseDatabase(abc.ABC):
|
|||||||
async def delete_platform_session(self, session_id: str) -> None:
|
async def delete_platform_session(self, session_id: str) -> None:
|
||||||
"""Delete a Platform session by its ID."""
|
"""Delete a Platform session by its ID."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# ====
|
||||||
|
# ChatUI Project Management
|
||||||
|
# ====
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def create_chatui_project(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
title: str,
|
||||||
|
emoji: str | None = "📁",
|
||||||
|
description: str | None = None,
|
||||||
|
) -> ChatUIProject:
|
||||||
|
"""Create a new ChatUI project."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_chatui_project_by_id(self, project_id: str) -> ChatUIProject | None:
|
||||||
|
"""Get a ChatUI project by its ID."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_chatui_projects_by_creator(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
) -> list[ChatUIProject]:
|
||||||
|
"""Get all ChatUI projects for a specific creator."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def update_chatui_project(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
title: str | None = None,
|
||||||
|
emoji: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Update a ChatUI project."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete_chatui_project(self, project_id: str) -> None:
|
||||||
|
"""Delete a ChatUI project by its ID."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def add_session_to_project(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
project_id: str,
|
||||||
|
) -> SessionProjectRelation:
|
||||||
|
"""Add a session to a project."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def remove_session_from_project(self, session_id: str) -> None:
|
||||||
|
"""Remove a session from its project."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_project_sessions(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
) -> list[PlatformSession]:
|
||||||
|
"""Get all sessions in a project."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_project_by_session(
|
||||||
|
self, session_id: str, creator: str
|
||||||
|
) -> ChatUIProject | None:
|
||||||
|
"""Get the project that a session belongs to."""
|
||||||
|
...
|
||||||
|
|||||||
@@ -239,6 +239,71 @@ class Attachment(SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatUIProject(SQLModel, table=True):
|
||||||
|
"""This class represents projects for organizing ChatUI conversations.
|
||||||
|
|
||||||
|
Projects allow users to group related conversations together.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "chatui_projects"
|
||||||
|
|
||||||
|
inner_id: int | None = Field(
|
||||||
|
primary_key=True,
|
||||||
|
sa_column_kwargs={"autoincrement": True},
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
project_id: str = Field(
|
||||||
|
max_length=36,
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
)
|
||||||
|
creator: str = Field(nullable=False)
|
||||||
|
"""Username of the project creator"""
|
||||||
|
emoji: str | None = Field(default="📁", max_length=10)
|
||||||
|
"""Emoji icon for the project"""
|
||||||
|
title: str = Field(nullable=False, max_length=255)
|
||||||
|
"""Title of the project"""
|
||||||
|
description: str | None = Field(default=None, max_length=1000)
|
||||||
|
"""Description of the project"""
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
updated_at: datetime = Field(
|
||||||
|
default_factory=lambda: datetime.now(timezone.utc),
|
||||||
|
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"project_id",
|
||||||
|
name="uix_chatui_project_id",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SessionProjectRelation(SQLModel, table=True):
|
||||||
|
"""This class represents the relationship between platform sessions and ChatUI projects."""
|
||||||
|
|
||||||
|
__tablename__: str = "session_project_relations"
|
||||||
|
|
||||||
|
id: int | None = Field(
|
||||||
|
primary_key=True,
|
||||||
|
sa_column_kwargs={"autoincrement": True},
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
session_id: str = Field(nullable=False, max_length=100)
|
||||||
|
"""Session ID from PlatformSession"""
|
||||||
|
project_id: str = Field(nullable=False, max_length=36)
|
||||||
|
"""Project ID from ChatUIProject"""
|
||||||
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"session_id",
|
||||||
|
name="uix_session_project_relation",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandConfig(SQLModel, table=True):
|
class CommandConfig(SQLModel, table=True):
|
||||||
"""Per-command configuration overrides for dashboard management."""
|
"""Per-command configuration overrides for dashboard management."""
|
||||||
|
|
||||||
|
|||||||
+225
-4
@@ -11,6 +11,7 @@ from sqlmodel import col, delete, desc, func, or_, select, text, update
|
|||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.db.po import (
|
from astrbot.core.db.po import (
|
||||||
Attachment,
|
Attachment,
|
||||||
|
ChatUIProject,
|
||||||
CommandConfig,
|
CommandConfig,
|
||||||
CommandConflict,
|
CommandConflict,
|
||||||
ConversationV2,
|
ConversationV2,
|
||||||
@@ -19,6 +20,7 @@ from astrbot.core.db.po import (
|
|||||||
PlatformSession,
|
PlatformSession,
|
||||||
PlatformStat,
|
PlatformStat,
|
||||||
Preference,
|
Preference,
|
||||||
|
SessionProjectRelation,
|
||||||
SQLModel,
|
SQLModel,
|
||||||
)
|
)
|
||||||
from astrbot.core.db.po import (
|
from astrbot.core.db.po import (
|
||||||
@@ -1060,12 +1062,35 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
platform_id: str | None = None,
|
platform_id: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
) -> list[PlatformSession]:
|
) -> list[dict]:
|
||||||
"""Get all Platform sessions for a specific creator (username) and optionally platform."""
|
"""Get all Platform sessions for a specific creator (username) and optionally platform.
|
||||||
|
|
||||||
|
Returns a list of dicts containing session info and project info (if session belongs to a project).
|
||||||
|
"""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
query = select(PlatformSession).where(PlatformSession.creator == creator)
|
|
||||||
|
# LEFT JOIN with SessionProjectRelation and ChatUIProject to get project info
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
PlatformSession,
|
||||||
|
col(ChatUIProject.project_id),
|
||||||
|
col(ChatUIProject.title).label("project_title"),
|
||||||
|
col(ChatUIProject.emoji).label("project_emoji"),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
SessionProjectRelation,
|
||||||
|
col(PlatformSession.session_id)
|
||||||
|
== col(SessionProjectRelation.session_id),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
ChatUIProject,
|
||||||
|
col(SessionProjectRelation.project_id)
|
||||||
|
== col(ChatUIProject.project_id),
|
||||||
|
)
|
||||||
|
.where(col(PlatformSession.creator) == creator)
|
||||||
|
)
|
||||||
|
|
||||||
if platform_id:
|
if platform_id:
|
||||||
query = query.where(PlatformSession.platform_id == platform_id)
|
query = query.where(PlatformSession.platform_id == platform_id)
|
||||||
@@ -1076,7 +1101,24 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
)
|
)
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return list(result.scalars().all())
|
|
||||||
|
# Convert to list of dicts with session and project info
|
||||||
|
sessions_with_projects = []
|
||||||
|
for row in result.all():
|
||||||
|
platform_session = row[0]
|
||||||
|
project_id = row[1]
|
||||||
|
project_title = row[2]
|
||||||
|
project_emoji = row[3]
|
||||||
|
|
||||||
|
session_dict = {
|
||||||
|
"session": platform_session,
|
||||||
|
"project_id": project_id,
|
||||||
|
"project_title": project_title,
|
||||||
|
"project_emoji": project_emoji,
|
||||||
|
}
|
||||||
|
sessions_with_projects.append(session_dict)
|
||||||
|
|
||||||
|
return sessions_with_projects
|
||||||
|
|
||||||
async def update_platform_session(
|
async def update_platform_session(
|
||||||
self,
|
self,
|
||||||
@@ -1107,3 +1149,182 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
col(PlatformSession.session_id) == session_id,
|
col(PlatformSession.session_id) == session_id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ====
|
||||||
|
# ChatUI Project Management
|
||||||
|
# ====
|
||||||
|
|
||||||
|
async def create_chatui_project(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
title: str,
|
||||||
|
emoji: str | None = "📁",
|
||||||
|
description: str | None = None,
|
||||||
|
) -> ChatUIProject:
|
||||||
|
"""Create a new ChatUI project."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
project = ChatUIProject(
|
||||||
|
creator=creator,
|
||||||
|
title=title,
|
||||||
|
emoji=emoji,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
session.add(project)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(project)
|
||||||
|
return project
|
||||||
|
|
||||||
|
async def get_chatui_project_by_id(self, project_id: str) -> ChatUIProject | None:
|
||||||
|
"""Get a ChatUI project by its ID."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
result = await session.execute(
|
||||||
|
select(ChatUIProject).where(
|
||||||
|
col(ChatUIProject.project_id) == project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_chatui_projects_by_creator(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
) -> list[ChatUIProject]:
|
||||||
|
"""Get all ChatUI projects for a specific creator."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
result = await session.execute(
|
||||||
|
select(ChatUIProject)
|
||||||
|
.where(col(ChatUIProject.creator) == creator)
|
||||||
|
.order_by(desc(ChatUIProject.updated_at))
|
||||||
|
.limit(page_size)
|
||||||
|
.offset(offset),
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def update_chatui_project(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
title: str | None = None,
|
||||||
|
emoji: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Update a ChatUI project."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
values: dict[str, T.Any] = {"updated_at": datetime.now(timezone.utc)}
|
||||||
|
if title is not None:
|
||||||
|
values["title"] = title
|
||||||
|
if emoji is not None:
|
||||||
|
values["emoji"] = emoji
|
||||||
|
if description is not None:
|
||||||
|
values["description"] = description
|
||||||
|
|
||||||
|
await session.execute(
|
||||||
|
update(ChatUIProject)
|
||||||
|
.where(col(ChatUIProject.project_id) == project_id)
|
||||||
|
.values(**values),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_chatui_project(self, project_id: str) -> None:
|
||||||
|
"""Delete a ChatUI project by its ID."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
# First remove all session relations
|
||||||
|
await session.execute(
|
||||||
|
delete(SessionProjectRelation).where(
|
||||||
|
col(SessionProjectRelation.project_id) == project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Then delete the project
|
||||||
|
await session.execute(
|
||||||
|
delete(ChatUIProject).where(
|
||||||
|
col(ChatUIProject.project_id) == project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def add_session_to_project(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
project_id: str,
|
||||||
|
) -> SessionProjectRelation:
|
||||||
|
"""Add a session to a project."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
# First remove existing relation if any
|
||||||
|
await session.execute(
|
||||||
|
delete(SessionProjectRelation).where(
|
||||||
|
col(SessionProjectRelation.session_id) == session_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Then create new relation
|
||||||
|
relation = SessionProjectRelation(
|
||||||
|
session_id=session_id,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
session.add(relation)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(relation)
|
||||||
|
return relation
|
||||||
|
|
||||||
|
async def remove_session_from_project(self, session_id: str) -> None:
|
||||||
|
"""Remove a session from its project."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
await session.execute(
|
||||||
|
delete(SessionProjectRelation).where(
|
||||||
|
col(SessionProjectRelation.session_id) == session_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_project_sessions(
|
||||||
|
self,
|
||||||
|
project_id: str,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
) -> list[PlatformSession]:
|
||||||
|
"""Get all sessions in a project."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
result = await session.execute(
|
||||||
|
select(PlatformSession)
|
||||||
|
.join(
|
||||||
|
SessionProjectRelation,
|
||||||
|
col(PlatformSession.session_id)
|
||||||
|
== col(SessionProjectRelation.session_id),
|
||||||
|
)
|
||||||
|
.where(col(SessionProjectRelation.project_id) == project_id)
|
||||||
|
.order_by(desc(PlatformSession.updated_at))
|
||||||
|
.limit(page_size)
|
||||||
|
.offset(offset),
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_project_by_session(
|
||||||
|
self, session_id: str, creator: str
|
||||||
|
) -> ChatUIProject | None:
|
||||||
|
"""Get the project that a session belongs to."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
result = await session.execute(
|
||||||
|
select(ChatUIProject)
|
||||||
|
.join(
|
||||||
|
SessionProjectRelation,
|
||||||
|
col(ChatUIProject.project_id)
|
||||||
|
== col(SessionProjectRelation.project_id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
col(SessionProjectRelation.session_id) == session_id,
|
||||||
|
col(ChatUIProject.creator) == creator,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from .auth import AuthRoute
|
from .auth import AuthRoute
|
||||||
from .backup import BackupRoute
|
from .backup import BackupRoute
|
||||||
from .chat import ChatRoute
|
from .chat import ChatRoute
|
||||||
|
from .chatui_project import ChatUIProjectRoute
|
||||||
from .command import CommandRoute
|
from .command import CommandRoute
|
||||||
from .config import ConfigRoute
|
from .config import ConfigRoute
|
||||||
from .conversation import ConversationRoute
|
from .conversation import ConversationRoute
|
||||||
@@ -20,6 +21,7 @@ __all__ = [
|
|||||||
"AuthRoute",
|
"AuthRoute",
|
||||||
"BackupRoute",
|
"BackupRoute",
|
||||||
"ChatRoute",
|
"ChatRoute",
|
||||||
|
"ChatUIProjectRoute",
|
||||||
"CommandRoute",
|
"CommandRoute",
|
||||||
"ConfigRoute",
|
"ConfigRoute",
|
||||||
"ConversationRoute",
|
"ConversationRoute",
|
||||||
|
|||||||
@@ -618,9 +618,17 @@ class ChatRoute(Route):
|
|||||||
page_size=100, # 暂时返回前100个
|
page_size=100, # 暂时返回前100个
|
||||||
)
|
)
|
||||||
|
|
||||||
# 转换为字典格式,并添加额外信息
|
# 转换为字典格式,并添加项目信息
|
||||||
|
# get_platform_sessions_by_creator 现在返回 list[dict] 包含 session 和项目字段
|
||||||
sessions_data = []
|
sessions_data = []
|
||||||
for session in sessions:
|
for item in sessions:
|
||||||
|
session = item["session"]
|
||||||
|
project_id = item["project_id"]
|
||||||
|
|
||||||
|
# 跳过属于项目的会话(在侧边栏对话列表中不显示)
|
||||||
|
if project_id is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
sessions_data.append(
|
sessions_data.append(
|
||||||
{
|
{
|
||||||
"session_id": session.session_id,
|
"session_id": session.session_id,
|
||||||
@@ -645,6 +653,12 @@ class ChatRoute(Route):
|
|||||||
session = await self.db.get_platform_session_by_id(session_id)
|
session = await self.db.get_platform_session_by_id(session_id)
|
||||||
platform_id = session.platform_id if session else "webchat"
|
platform_id = session.platform_id if session else "webchat"
|
||||||
|
|
||||||
|
# 获取项目信息(如果会话属于某个项目)
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
project_info = await self.db.get_project_by_session(
|
||||||
|
session_id=session_id, creator=username
|
||||||
|
)
|
||||||
|
|
||||||
# Get platform message history using session_id
|
# Get platform message history using session_id
|
||||||
history_ls = await self.platform_history_mgr.get(
|
history_ls = await self.platform_history_mgr.get(
|
||||||
platform_id=platform_id,
|
platform_id=platform_id,
|
||||||
@@ -655,16 +669,20 @@ class ChatRoute(Route):
|
|||||||
|
|
||||||
history_res = [history.model_dump() for history in history_ls]
|
history_res = [history.model_dump() for history in history_ls]
|
||||||
|
|
||||||
return (
|
response_data = {
|
||||||
Response()
|
"history": history_res,
|
||||||
.ok(
|
"is_running": self.running_convs.get(session_id, False),
|
||||||
data={
|
}
|
||||||
"history": history_res,
|
|
||||||
"is_running": self.running_convs.get(session_id, False),
|
# 如果会话属于项目,添加项目信息
|
||||||
},
|
if project_info:
|
||||||
)
|
response_data["project"] = {
|
||||||
.__dict__
|
"project_id": project_info.project_id,
|
||||||
)
|
"title": project_info.title,
|
||||||
|
"emoji": project_info.emoji,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response().ok(data=response_data).__dict__
|
||||||
|
|
||||||
async def update_session_display_name(self):
|
async def update_session_display_name(self):
|
||||||
"""Update a Platform session's display name."""
|
"""Update a Platform session's display name."""
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
from quart import g, request
|
||||||
|
|
||||||
|
from astrbot.core.db import BaseDatabase
|
||||||
|
|
||||||
|
from .route import Response, Route, RouteContext
|
||||||
|
|
||||||
|
|
||||||
|
class ChatUIProjectRoute(Route):
|
||||||
|
def __init__(self, context: RouteContext, db: BaseDatabase) -> None:
|
||||||
|
super().__init__(context)
|
||||||
|
self.routes = {
|
||||||
|
"/chatui_project/create": ("POST", self.create_project),
|
||||||
|
"/chatui_project/list": ("GET", self.list_projects),
|
||||||
|
"/chatui_project/get": ("GET", self.get_project),
|
||||||
|
"/chatui_project/update": ("POST", self.update_chatui_project),
|
||||||
|
"/chatui_project/delete": ("GET", self.delete_project),
|
||||||
|
"/chatui_project/add_session": ("POST", self.add_session_to_project),
|
||||||
|
"/chatui_project/remove_session": (
|
||||||
|
"POST",
|
||||||
|
self.remove_session_from_project,
|
||||||
|
),
|
||||||
|
"/chatui_project/get_sessions": ("GET", self.get_project_sessions),
|
||||||
|
}
|
||||||
|
self.db = db
|
||||||
|
self.register_routes()
|
||||||
|
|
||||||
|
async def create_project(self):
|
||||||
|
"""Create a new ChatUI project."""
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
post_data = await request.json
|
||||||
|
|
||||||
|
title = post_data.get("title")
|
||||||
|
emoji = post_data.get("emoji", "📁")
|
||||||
|
description = post_data.get("description")
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
return Response().error("Missing key: title").__dict__
|
||||||
|
|
||||||
|
project = await self.db.create_chatui_project(
|
||||||
|
creator=username,
|
||||||
|
title=title,
|
||||||
|
emoji=emoji,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.ok(
|
||||||
|
data={
|
||||||
|
"project_id": project.project_id,
|
||||||
|
"title": project.title,
|
||||||
|
"emoji": project.emoji,
|
||||||
|
"description": project.description,
|
||||||
|
"created_at": project.created_at.astimezone().isoformat(),
|
||||||
|
"updated_at": project.updated_at.astimezone().isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
async def list_projects(self):
|
||||||
|
"""Get all ChatUI projects for the current user."""
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
projects = await self.db.get_chatui_projects_by_creator(creator=username)
|
||||||
|
|
||||||
|
projects_data = [
|
||||||
|
{
|
||||||
|
"project_id": project.project_id,
|
||||||
|
"title": project.title,
|
||||||
|
"emoji": project.emoji,
|
||||||
|
"description": project.description,
|
||||||
|
"created_at": project.created_at.astimezone().isoformat(),
|
||||||
|
"updated_at": project.updated_at.astimezone().isoformat(),
|
||||||
|
}
|
||||||
|
for project in projects
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response().ok(data=projects_data).__dict__
|
||||||
|
|
||||||
|
async def get_project(self):
|
||||||
|
"""Get a specific ChatUI project."""
|
||||||
|
project_id = request.args.get("project_id")
|
||||||
|
if not project_id:
|
||||||
|
return Response().error("Missing key: project_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
project = await self.db.get_chatui_project_by_id(project_id)
|
||||||
|
if not project:
|
||||||
|
return Response().error(f"Project {project_id} not found").__dict__
|
||||||
|
|
||||||
|
# Verify ownership
|
||||||
|
if project.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.ok(
|
||||||
|
data={
|
||||||
|
"project_id": project.project_id,
|
||||||
|
"title": project.title,
|
||||||
|
"emoji": project.emoji,
|
||||||
|
"description": project.description,
|
||||||
|
"created_at": project.created_at.astimezone().isoformat(),
|
||||||
|
"updated_at": project.updated_at.astimezone().isoformat(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_chatui_project(self):
|
||||||
|
"""Update a ChatUI project."""
|
||||||
|
post_data = await request.json
|
||||||
|
|
||||||
|
project_id = post_data.get("project_id")
|
||||||
|
title = post_data.get("title")
|
||||||
|
emoji = post_data.get("emoji")
|
||||||
|
description = post_data.get("description")
|
||||||
|
|
||||||
|
if not project_id:
|
||||||
|
return Response().error("Missing key: project_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
# Verify ownership
|
||||||
|
project = await self.db.get_chatui_project_by_id(project_id)
|
||||||
|
if not project:
|
||||||
|
return Response().error(f"Project {project_id} not found").__dict__
|
||||||
|
if project.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
await self.db.update_chatui_project(
|
||||||
|
project_id=project_id,
|
||||||
|
title=title,
|
||||||
|
emoji=emoji,
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response().ok().__dict__
|
||||||
|
|
||||||
|
async def delete_project(self):
|
||||||
|
"""Delete a ChatUI project."""
|
||||||
|
project_id = request.args.get("project_id")
|
||||||
|
if not project_id:
|
||||||
|
return Response().error("Missing key: project_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
# Verify ownership
|
||||||
|
project = await self.db.get_chatui_project_by_id(project_id)
|
||||||
|
if not project:
|
||||||
|
return Response().error(f"Project {project_id} not found").__dict__
|
||||||
|
if project.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
await self.db.delete_chatui_project(project_id)
|
||||||
|
|
||||||
|
return Response().ok().__dict__
|
||||||
|
|
||||||
|
async def add_session_to_project(self):
|
||||||
|
"""Add a session to a project."""
|
||||||
|
post_data = await request.json
|
||||||
|
|
||||||
|
session_id = post_data.get("session_id")
|
||||||
|
project_id = post_data.get("project_id")
|
||||||
|
|
||||||
|
if not session_id:
|
||||||
|
return Response().error("Missing key: session_id").__dict__
|
||||||
|
if not project_id:
|
||||||
|
return Response().error("Missing key: project_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
# Verify project ownership
|
||||||
|
project = await self.db.get_chatui_project_by_id(project_id)
|
||||||
|
if not project:
|
||||||
|
return Response().error(f"Project {project_id} not found").__dict__
|
||||||
|
if project.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
# Verify session ownership
|
||||||
|
session = await self.db.get_platform_session_by_id(session_id)
|
||||||
|
if not session:
|
||||||
|
return Response().error(f"Session {session_id} not found").__dict__
|
||||||
|
if session.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
await self.db.add_session_to_project(session_id, project_id)
|
||||||
|
|
||||||
|
return Response().ok().__dict__
|
||||||
|
|
||||||
|
async def remove_session_from_project(self):
|
||||||
|
"""Remove a session from its project."""
|
||||||
|
post_data = await request.json
|
||||||
|
|
||||||
|
session_id = post_data.get("session_id")
|
||||||
|
|
||||||
|
if not session_id:
|
||||||
|
return Response().error("Missing key: session_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
# Verify session ownership
|
||||||
|
session = await self.db.get_platform_session_by_id(session_id)
|
||||||
|
if not session:
|
||||||
|
return Response().error(f"Session {session_id} not found").__dict__
|
||||||
|
if session.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
await self.db.remove_session_from_project(session_id)
|
||||||
|
|
||||||
|
return Response().ok().__dict__
|
||||||
|
|
||||||
|
async def get_project_sessions(self):
|
||||||
|
"""Get all sessions in a project."""
|
||||||
|
project_id = request.args.get("project_id")
|
||||||
|
if not project_id:
|
||||||
|
return Response().error("Missing key: project_id").__dict__
|
||||||
|
|
||||||
|
username = g.get("username", "guest")
|
||||||
|
|
||||||
|
# Verify project ownership
|
||||||
|
project = await self.db.get_chatui_project_by_id(project_id)
|
||||||
|
if not project:
|
||||||
|
return Response().error(f"Project {project_id} not found").__dict__
|
||||||
|
if project.creator != username:
|
||||||
|
return Response().error("Permission denied").__dict__
|
||||||
|
|
||||||
|
sessions = await self.db.get_project_sessions(project_id)
|
||||||
|
|
||||||
|
sessions_data = [
|
||||||
|
{
|
||||||
|
"session_id": session.session_id,
|
||||||
|
"platform_id": session.platform_id,
|
||||||
|
"creator": session.creator,
|
||||||
|
"display_name": session.display_name,
|
||||||
|
"is_group": session.is_group,
|
||||||
|
"created_at": session.created_at.astimezone().isoformat(),
|
||||||
|
"updated_at": session.updated_at.astimezone().isoformat(),
|
||||||
|
}
|
||||||
|
for session in sessions
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response().ok(data=sessions_data).__dict__
|
||||||
@@ -74,6 +74,7 @@ class AstrBotDashboard:
|
|||||||
self.sfr = StaticFileRoute(self.context)
|
self.sfr = StaticFileRoute(self.context)
|
||||||
self.ar = AuthRoute(self.context)
|
self.ar = AuthRoute(self.context)
|
||||||
self.chat_route = ChatRoute(self.context, db, core_lifecycle)
|
self.chat_route = ChatRoute(self.context, db, core_lifecycle)
|
||||||
|
self.chatui_project_route = ChatUIProjectRoute(self.context, db)
|
||||||
self.tools_root = ToolsRoute(self.context, core_lifecycle)
|
self.tools_root = ToolsRoute(self.context, core_lifecycle)
|
||||||
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
|
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
|
||||||
self.file_route = FileRoute(self.context)
|
self.file_route = FileRoute(self.context)
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
:sessions="sessions"
|
:sessions="sessions"
|
||||||
:selectedSessions="selectedSessions"
|
:selectedSessions="selectedSessions"
|
||||||
:currSessionId="currSessionId"
|
:currSessionId="currSessionId"
|
||||||
|
:selectedProjectId="selectedProjectId"
|
||||||
:isDark="isDark"
|
:isDark="isDark"
|
||||||
:chatboxMode="chatboxMode"
|
:chatboxMode="chatboxMode"
|
||||||
:isMobile="isMobile"
|
:isMobile="isMobile"
|
||||||
:mobileMenuOpen="mobileMenuOpen"
|
:mobileMenuOpen="mobileMenuOpen"
|
||||||
|
:projects="projects"
|
||||||
@newChat="handleNewChat"
|
@newChat="handleNewChat"
|
||||||
@selectConversation="handleSelectConversation"
|
@selectConversation="handleSelectConversation"
|
||||||
@editTitle="showEditTitleDialog"
|
@editTitle="showEditTitleDialog"
|
||||||
@@ -20,6 +22,10 @@
|
|||||||
@closeMobileSidebar="closeMobileSidebar"
|
@closeMobileSidebar="closeMobileSidebar"
|
||||||
@toggleTheme="toggleTheme"
|
@toggleTheme="toggleTheme"
|
||||||
@toggleFullscreen="toggleFullscreen"
|
@toggleFullscreen="toggleFullscreen"
|
||||||
|
@selectProject="handleSelectProject"
|
||||||
|
@createProject="showCreateProjectDialog"
|
||||||
|
@editProject="showEditProjectDialog"
|
||||||
|
@deleteProject="handleDeleteProject"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 右侧聊天内容区域 -->
|
<!-- 右侧聊天内容区域 -->
|
||||||
@@ -32,7 +38,17 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="message-list-wrapper" v-if="messages && messages.length > 0">
|
<!-- 面包屑导航 -->
|
||||||
|
<div v-if="currentSessionProject && messages && messages.length > 0" class="breadcrumb-container">
|
||||||
|
<div class="breadcrumb-content">
|
||||||
|
<span class="breadcrumb-emoji">{{ currentSessionProject.emoji || '📁' }}</span>
|
||||||
|
<span class="breadcrumb-project" @click="handleSelectProject(currentSessionProject.project_id)">{{ currentSessionProject.title }}</span>
|
||||||
|
<v-icon size="small" class="breadcrumb-separator">mdi-chevron-right</v-icon>
|
||||||
|
<span class="breadcrumb-session">{{ getCurrentSession?.display_name || tm('conversation.newConversation') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message-list-wrapper" v-if="currSessionId && !selectedProjectId">
|
||||||
<MessageList :messages="messages" :isDark="isDark"
|
<MessageList :messages="messages" :isDark="isDark"
|
||||||
:isStreaming="isStreaming || isConvRunning"
|
:isStreaming="isStreaming || isConvRunning"
|
||||||
:isLoadingMessages="isLoadingMessages"
|
:isLoadingMessages="isLoadingMessages"
|
||||||
@@ -42,23 +58,70 @@
|
|||||||
ref="messageList" />
|
ref="messageList" />
|
||||||
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
<div class="message-list-fade" :class="{ 'fade-dark': isDark }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="welcome-container fade-in" v-else>
|
<ProjectView
|
||||||
<div v-if="isLoadingMessages" class="loading-overlay-welcome">
|
v-else-if="selectedProjectId"
|
||||||
<v-progress-circular
|
:project="currentProject"
|
||||||
indeterminate
|
:sessions="projectSessions"
|
||||||
size="48"
|
@selectSession="(sessionId) => handleSelectConversation([sessionId])"
|
||||||
width="4"
|
@editSessionTitle="showEditTitleDialog"
|
||||||
color="primary"
|
@deleteSession="handleDeleteConversation"
|
||||||
></v-progress-circular>
|
>
|
||||||
</div>
|
<ChatInput
|
||||||
<div v-else class="welcome-title">
|
v-model:prompt="prompt"
|
||||||
<span>Hello, I'm</span>
|
:stagedImagesUrl="stagedImagesUrl"
|
||||||
<span class="bot-name">AstrBot ⭐</span>
|
:stagedAudioUrl="stagedAudioUrl"
|
||||||
</div>
|
:stagedFiles="stagedNonImageFiles"
|
||||||
</div>
|
:disabled="isStreaming"
|
||||||
|
:enableStreaming="enableStreaming"
|
||||||
|
:isRecording="isRecording"
|
||||||
|
:session-id="currSessionId || null"
|
||||||
|
:current-session="getCurrentSession"
|
||||||
|
:replyTo="replyTo"
|
||||||
|
@send="handleSendMessage"
|
||||||
|
@toggleStreaming="toggleStreaming"
|
||||||
|
@removeImage="removeImage"
|
||||||
|
@removeAudio="removeAudio"
|
||||||
|
@removeFile="removeFile"
|
||||||
|
@startRecording="handleStartRecording"
|
||||||
|
@stopRecording="handleStopRecording"
|
||||||
|
@pasteImage="handlePaste"
|
||||||
|
@fileSelect="handleFileSelect"
|
||||||
|
@clearReply="clearReply"
|
||||||
|
ref="chatInputRef"
|
||||||
|
/>
|
||||||
|
</ProjectView>
|
||||||
|
<WelcomeView
|
||||||
|
v-else
|
||||||
|
:isLoading="isLoadingMessages"
|
||||||
|
>
|
||||||
|
<ChatInput
|
||||||
|
v-model:prompt="prompt"
|
||||||
|
:stagedImagesUrl="stagedImagesUrl"
|
||||||
|
:stagedAudioUrl="stagedAudioUrl"
|
||||||
|
:stagedFiles="stagedNonImageFiles"
|
||||||
|
:disabled="isStreaming"
|
||||||
|
:enableStreaming="enableStreaming"
|
||||||
|
:isRecording="isRecording"
|
||||||
|
:session-id="currSessionId || null"
|
||||||
|
:current-session="getCurrentSession"
|
||||||
|
:replyTo="replyTo"
|
||||||
|
@send="handleSendMessage"
|
||||||
|
@toggleStreaming="toggleStreaming"
|
||||||
|
@removeImage="removeImage"
|
||||||
|
@removeAudio="removeAudio"
|
||||||
|
@removeFile="removeFile"
|
||||||
|
@startRecording="handleStartRecording"
|
||||||
|
@stopRecording="handleStopRecording"
|
||||||
|
@pasteImage="handlePaste"
|
||||||
|
@fileSelect="handleFileSelect"
|
||||||
|
@clearReply="clearReply"
|
||||||
|
ref="chatInputRef"
|
||||||
|
/>
|
||||||
|
</WelcomeView>
|
||||||
|
|
||||||
<!-- 输入区域 -->
|
<!-- 输入区域 -->
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
v-if="currSessionId && !selectedProjectId"
|
||||||
v-model:prompt="prompt"
|
v-model:prompt="prompt"
|
||||||
:stagedImagesUrl="stagedImagesUrl"
|
:stagedImagesUrl="stagedImagesUrl"
|
||||||
:stagedAudioUrl="stagedAudioUrl"
|
:stagedAudioUrl="stagedAudioUrl"
|
||||||
@@ -114,6 +177,13 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- 创建/编辑项目对话框 -->
|
||||||
|
<ProjectDialog
|
||||||
|
v-model="projectDialog"
|
||||||
|
:project="editingProject"
|
||||||
|
@save="handleSaveProject"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -122,14 +192,19 @@ import { useRouter, useRoute } from 'vue-router';
|
|||||||
import { useCustomizerStore } from '@/stores/customizer';
|
import { useCustomizerStore } from '@/stores/customizer';
|
||||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
import { useTheme } from 'vuetify';
|
import { useTheme } from 'vuetify';
|
||||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
|
||||||
import MessageList from '@/components/chat/MessageList.vue';
|
import MessageList from '@/components/chat/MessageList.vue';
|
||||||
import ConversationSidebar from '@/components/chat/ConversationSidebar.vue';
|
import ConversationSidebar from '@/components/chat/ConversationSidebar.vue';
|
||||||
import ChatInput from '@/components/chat/ChatInput.vue';
|
import ChatInput from '@/components/chat/ChatInput.vue';
|
||||||
|
import ProjectDialog from '@/components/chat/ProjectDialog.vue';
|
||||||
|
import ProjectView from '@/components/chat/ProjectView.vue';
|
||||||
|
import WelcomeView from '@/components/chat/WelcomeView.vue';
|
||||||
|
import type { ProjectFormData } from '@/components/chat/ProjectDialog.vue';
|
||||||
import { useSessions } from '@/composables/useSessions';
|
import { useSessions } from '@/composables/useSessions';
|
||||||
import { useMessages } from '@/composables/useMessages';
|
import { useMessages } from '@/composables/useMessages';
|
||||||
import { useMediaHandling } from '@/composables/useMediaHandling';
|
import { useMediaHandling } from '@/composables/useMediaHandling';
|
||||||
import { useRecording } from '@/composables/useRecording';
|
import { useRecording } from '@/composables/useRecording';
|
||||||
|
import { useProjects } from '@/composables/useProjects';
|
||||||
|
import type { Project } from '@/components/chat/ProjectList.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chatboxMode?: boolean;
|
chatboxMode?: boolean;
|
||||||
@@ -189,11 +264,23 @@ const {
|
|||||||
|
|
||||||
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
|
const { isRecording, startRecording: startRec, stopRecording: stopRec } = useRecording();
|
||||||
|
|
||||||
|
const {
|
||||||
|
projects,
|
||||||
|
selectedProjectId,
|
||||||
|
getProjects,
|
||||||
|
createProject,
|
||||||
|
updateProject,
|
||||||
|
deleteProject,
|
||||||
|
addSessionToProject,
|
||||||
|
getProjectSessions
|
||||||
|
} = useProjects();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
isConvRunning,
|
isConvRunning,
|
||||||
enableStreaming,
|
enableStreaming,
|
||||||
|
currentSessionProject,
|
||||||
getSessionMessages: getSessionMsg,
|
getSessionMessages: getSessionMsg,
|
||||||
sendMessage: sendMsg,
|
sendMessage: sendMsg,
|
||||||
toggleStreaming
|
toggleStreaming
|
||||||
@@ -206,6 +293,14 @@ const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null);
|
|||||||
// 输入状态
|
// 输入状态
|
||||||
const prompt = ref('');
|
const prompt = ref('');
|
||||||
|
|
||||||
|
// 项目状态
|
||||||
|
const projectDialog = ref(false);
|
||||||
|
const editingProject = ref<Project | null>(null);
|
||||||
|
const projectSessions = ref<any[]>([]);
|
||||||
|
const currentProject = computed(() =>
|
||||||
|
projects.value.find(p => p.project_id === selectedProjectId.value)
|
||||||
|
);
|
||||||
|
|
||||||
// 引用消息状态
|
// 引用消息状态
|
||||||
interface ReplyInfo {
|
interface ReplyInfo {
|
||||||
messageId: number; // PlatformSessionHistoryMessage 的 id
|
messageId: number; // PlatformSessionHistoryMessage 的 id
|
||||||
@@ -304,6 +399,10 @@ function handleReplyWithText(replyData: any) {
|
|||||||
async function handleSelectConversation(sessionIds: string[]) {
|
async function handleSelectConversation(sessionIds: string[]) {
|
||||||
if (!sessionIds[0]) return;
|
if (!sessionIds[0]) return;
|
||||||
|
|
||||||
|
// 退出项目视图
|
||||||
|
selectedProjectId.value = null;
|
||||||
|
projectSessions.value = [];
|
||||||
|
|
||||||
// 立即更新选中状态,避免需要点击两次
|
// 立即更新选中状态,避免需要点击两次
|
||||||
currSessionId.value = sessionIds[0];
|
currSessionId.value = sessionIds[0];
|
||||||
selectedSessions.value = [sessionIds[0]];
|
selectedSessions.value = [sessionIds[0]];
|
||||||
@@ -340,6 +439,9 @@ function handleNewChat() {
|
|||||||
newChat(closeMobileSidebar);
|
newChat(closeMobileSidebar);
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
clearReply();
|
clearReply();
|
||||||
|
// 退出项目视图
|
||||||
|
selectedProjectId.value = null;
|
||||||
|
projectSessions.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteConversation(sessionId: string) {
|
async function handleDeleteConversation(sessionId: string) {
|
||||||
@@ -347,6 +449,53 @@ async function handleDeleteConversation(sessionId: string) {
|
|||||||
messages.value = [];
|
messages.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSelectProject(projectId: string) {
|
||||||
|
selectedProjectId.value = projectId;
|
||||||
|
const sessions = await getProjectSessions(projectId);
|
||||||
|
projectSessions.value = sessions;
|
||||||
|
messages.value = [];
|
||||||
|
|
||||||
|
// 清空当前会话ID,准备在项目中创建新对话
|
||||||
|
currSessionId.value = '';
|
||||||
|
selectedSessions.value = [];
|
||||||
|
|
||||||
|
// 手机端关闭侧边栏
|
||||||
|
if (isMobile.value) {
|
||||||
|
closeMobileSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateProjectDialog() {
|
||||||
|
editingProject.value = null;
|
||||||
|
projectDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showEditProjectDialog(project: Project) {
|
||||||
|
editingProject.value = project;
|
||||||
|
projectDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveProject(formData: ProjectFormData, projectId?: string) {
|
||||||
|
if (projectId) {
|
||||||
|
await updateProject(
|
||||||
|
projectId,
|
||||||
|
formData.title,
|
||||||
|
formData.emoji,
|
||||||
|
formData.description
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await createProject(
|
||||||
|
formData.title,
|
||||||
|
formData.emoji,
|
||||||
|
formData.description
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteProject(projectId: string) {
|
||||||
|
await deleteProject(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleStartRecording() {
|
async function handleStartRecording() {
|
||||||
await startRec();
|
await startRec();
|
||||||
}
|
}
|
||||||
@@ -373,7 +522,8 @@ async function handleSendMessage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currSessionId.value) {
|
const isCreatingNewSession = !currSessionId.value;
|
||||||
|
if (isCreatingNewSession) {
|
||||||
await newSession();
|
await newSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +555,14 @@ async function handleSendMessage() {
|
|||||||
selectedModelName,
|
selectedModelName,
|
||||||
replyToSend
|
replyToSend
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果在项目视图中创建了新会话,自动添加到当前项目
|
||||||
|
if (isCreatingNewSession && selectedProjectId.value && currSessionId.value) {
|
||||||
|
await addSessionToProject(currSessionId.value, selectedProjectId.value);
|
||||||
|
// 刷新项目会话列表
|
||||||
|
const sessions = await getProjectSessions(selectedProjectId.value);
|
||||||
|
projectSessions.value = sessions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路由变化监听
|
// 路由变化监听
|
||||||
@@ -454,6 +612,7 @@ onMounted(() => {
|
|||||||
checkMobile();
|
checkMobile();
|
||||||
window.addEventListener('resize', checkMobile);
|
window.addEventListener('resize', checkMobile);
|
||||||
getSessions();
|
getSessions();
|
||||||
|
getProjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -568,30 +727,39 @@ onBeforeUnmount(() => {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-container {
|
.breadcrumb-container {
|
||||||
height: 100%;
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid var(--v-theme-border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
gap: 8px;
|
||||||
position: relative;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-title {
|
.breadcrumb-emoji {
|
||||||
font-size: 28px;
|
font-size: 16px;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-overlay-welcome {
|
.breadcrumb-project {
|
||||||
display: flex;
|
font-weight: 500;
|
||||||
justify-content: center;
|
cursor: pointer;
|
||||||
align-items: center;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bot-name {
|
.breadcrumb-project:hover {
|
||||||
font-weight: 700;
|
opacity: 0.7;
|
||||||
margin-left: 8px;
|
}
|
||||||
color: var(--v-theme-secondary);
|
|
||||||
|
.breadcrumb-separator {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-session {
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
|
|||||||
@@ -29,32 +29,62 @@
|
|||||||
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 12px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
|
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 12px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; padding: 6px 14px;">
|
||||||
<div style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px;">
|
<div style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px;">
|
||||||
<ConfigSelector
|
<!-- Settings Menu -->
|
||||||
:session-id="sessionId || null"
|
<StyledMenu offset="8" location="top start" :close-on-content-click="false">
|
||||||
:platform-id="sessionPlatformId"
|
<template v-slot:activator="{ props: activatorProps }">
|
||||||
:is-group="sessionIsGroup"
|
<v-btn
|
||||||
:initial-config-id="props.configId"
|
v-bind="activatorProps"
|
||||||
@config-changed="handleConfigChange"
|
icon="mdi-plus"
|
||||||
/>
|
variant="text"
|
||||||
|
color="deep-purple"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Upload Files -->
|
||||||
|
<v-list-item
|
||||||
|
class="styled-menu-item"
|
||||||
|
rounded="md"
|
||||||
|
@click="triggerImageInput"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon icon="mdi-file-upload-outline" size="small"></v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ tm('input.upload') }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
|
<!-- Config Selector in Menu -->
|
||||||
|
<ConfigSelector
|
||||||
|
:session-id="sessionId || null"
|
||||||
|
:platform-id="sessionPlatformId"
|
||||||
|
:is-group="sessionIsGroup"
|
||||||
|
:initial-config-id="props.configId"
|
||||||
|
@config-changed="handleConfigChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Streaming Toggle in Menu -->
|
||||||
|
<v-list-item
|
||||||
|
class="styled-menu-item"
|
||||||
|
rounded="md"
|
||||||
|
@click="$emit('toggleStreaming')"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon :icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'" size="small"></v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled') }}
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</StyledMenu>
|
||||||
|
|
||||||
<!-- Provider/Model Selector Menu -->
|
<!-- Provider/Model Selector Menu -->
|
||||||
<ProviderModelMenu v-if="showProviderSelector" ref="providerModelMenuRef" />
|
<ProviderModelMenu v-if="showProviderSelector" ref="providerModelMenuRef" />
|
||||||
|
|
||||||
<v-tooltip :text="enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled')" location="top">
|
|
||||||
<template v-slot:activator="{ props }">
|
|
||||||
<v-chip v-bind="props" @click="$emit('toggleStreaming')" size="x-small" class="streaming-toggle-chip">
|
|
||||||
<v-icon start :icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'" size="small"></v-icon>
|
|
||||||
{{ enableStreaming ? tm('streaming.on') : tm('streaming.off') }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center;">
|
<div style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center;">
|
||||||
<input type="file" ref="imageInputRef" @change="handleFileSelect"
|
<input type="file" ref="imageInputRef" @change="handleFileSelect"
|
||||||
style="display: none" multiple />
|
style="display: none" multiple />
|
||||||
<v-progress-circular v-if="disabled" indeterminate size="16" class="mr-1" width="1.5" />
|
<v-progress-circular v-if="disabled" indeterminate size="16" class="mr-1" width="1.5" />
|
||||||
<v-btn @click="triggerImageInput" icon="mdi-plus" variant="text" color="deep-purple"
|
|
||||||
class="add-btn" size="small" />
|
|
||||||
<v-btn @click="handleRecordClick"
|
<v-btn @click="handleRecordClick"
|
||||||
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
|
:icon="isRecording ? 'mdi-stop-circle' : 'mdi-microphone'" variant="text"
|
||||||
:color="isRecording ? 'error' : 'deep-purple'" class="record-btn" size="small" />
|
:color="isRecording ? 'error' : 'deep-purple'" class="record-btn" size="small" />
|
||||||
@@ -99,6 +129,7 @@ import { useModuleI18n } from '@/i18n/composables';
|
|||||||
import { useCustomizerStore } from '@/stores/customizer';
|
import { useCustomizerStore } from '@/stores/customizer';
|
||||||
import ConfigSelector from './ConfigSelector.vue';
|
import ConfigSelector from './ConfigSelector.vue';
|
||||||
import ProviderModelMenu from './ProviderModelMenu.vue';
|
import ProviderModelMenu from './ProviderModelMenu.vue';
|
||||||
|
import StyledMenu from '@/components/shared/StyledMenu.vue';
|
||||||
import type { Session } from '@/composables/useSessions';
|
import type { Session } from '@/composables/useSessions';
|
||||||
|
|
||||||
interface StagedFileInfo {
|
interface StagedFileInfo {
|
||||||
@@ -425,16 +456,6 @@ defineExpose({
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.streaming-toggle-chip {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.streaming-toggle-chip:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in {
|
.fade-in {
|
||||||
animation: fadeIn 0.3s ease-in-out;
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
@@ -458,11 +479,6 @@ defineExpose({
|
|||||||
.input-container {
|
.input-container {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
margin: 0 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
border-left: none !important;
|
|
||||||
border-right: none !important;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-tooltip text="选择用于当前会话的配置文件" location="top">
|
<v-list-item
|
||||||
<template #activator="{ props: tooltipProps }">
|
class="styled-menu-item"
|
||||||
<v-chip
|
rounded="md"
|
||||||
v-bind="tooltipProps"
|
@click="openDialog"
|
||||||
class="text-none config-chip"
|
:disabled="loadingConfigs || saving"
|
||||||
variant="tonal"
|
>
|
||||||
size="x-small"
|
<template v-slot:prepend>
|
||||||
rounded="lg"
|
<v-icon icon="mdi-cog-outline" size="small"></v-icon>
|
||||||
@click="openDialog"
|
|
||||||
:disabled="loadingConfigs || saving"
|
|
||||||
>
|
|
||||||
<v-icon start size="14">mdi-cog</v-icon>
|
|
||||||
{{ selectedConfigLabel }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
<v-list-item-title>
|
||||||
|
{{ tm('config.title') }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption">
|
||||||
|
{{ selectedConfigLabel }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon icon="mdi-chevron-right" size="small" class="text-medium-emphasis"></v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
|
||||||
<v-dialog v-model="dialog" max-width="480">
|
<v-dialog v-model="dialog" max-width="480">
|
||||||
<v-card>
|
<v-card>
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useToast } from '@/utils/toast';
|
import { useToast } from '@/utils/toast';
|
||||||
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
|
|
||||||
interface ConfigInfo {
|
interface ConfigInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -100,6 +104,8 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
const emit = defineEmits<{ 'config-changed': [ConfigChangedPayload] }>();
|
||||||
|
|
||||||
|
const { tm } = useModuleI18n('features/chat');
|
||||||
|
|
||||||
const configOptions = ref<ConfigInfo[]>([]);
|
const configOptions = ref<ConfigInfo[]>([]);
|
||||||
const loadingConfigs = ref(false);
|
const loadingConfigs = ref(false);
|
||||||
const dialog = ref(false);
|
const dialog = ref(false);
|
||||||
@@ -301,11 +307,6 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.config-chip {
|
|
||||||
cursor: pointer;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-list {
|
.config-list {
|
||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -21,12 +21,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="padding: 8px; opacity: 0.6;">
|
<div style="padding: 8px; opacity: 0.6;">
|
||||||
<v-btn block variant="text" class="new-chat-btn" @click="$emit('newChat')" :disabled="!currSessionId"
|
<v-btn block variant="text" class="new-chat-btn" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
|
||||||
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-square-edit-outline">{{ tm('actions.newChat') }}</v-btn>
|
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-square-edit-outline">{{ tm('actions.newChat') }}</v-btn>
|
||||||
<v-btn icon="mdi-square-edit-outline" rounded="xl" @click="$emit('newChat')" :disabled="!currSessionId"
|
<v-btn icon="mdi-square-edit-outline" rounded="xl" @click="$emit('newChat')" :disabled="!currSessionId && !selectedProjectId"
|
||||||
v-if="sidebarCollapsed && !isMobile" elevation="0"></v-btn>
|
v-if="sidebarCollapsed && !isMobile" elevation="0"></v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目列表组件 -->
|
||||||
|
<ProjectList
|
||||||
|
v-if="!sidebarCollapsed || isMobile"
|
||||||
|
:projects="projects"
|
||||||
|
@selectProject="$emit('selectProject', $event)"
|
||||||
|
@createProject="$emit('createProject')"
|
||||||
|
@editProject="$emit('editProject', $event)"
|
||||||
|
@deleteProject="$emit('deleteProject', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<div style="overflow-y: auto; flex-grow: 1;"
|
<div style="overflow-y: auto; flex-grow: 1;"
|
||||||
v-if="!sidebarCollapsed || isMobile">
|
v-if="!sidebarCollapsed || isMobile">
|
||||||
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
|
<v-card v-if="sessions.length > 0" flat style="background-color: transparent;">
|
||||||
@@ -137,18 +147,24 @@ import type { Session } from '@/composables/useSessions';
|
|||||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||||
import StyledMenu from '@/components/shared/StyledMenu.vue';
|
import StyledMenu from '@/components/shared/StyledMenu.vue';
|
||||||
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
||||||
|
import ProjectList from '@/components/chat/ProjectList.vue';
|
||||||
|
import type { Project } from '@/components/chat/ProjectList.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sessions: Session[];
|
sessions: Session[];
|
||||||
selectedSessions: string[];
|
selectedSessions: string[];
|
||||||
currSessionId: string;
|
currSessionId: string;
|
||||||
|
selectedProjectId?: string | null;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
chatboxMode: boolean;
|
chatboxMode: boolean;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
mobileMenuOpen: boolean;
|
mobileMenuOpen: boolean;
|
||||||
|
projects?: Project[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
projects: () => []
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
newChat: [];
|
newChat: [];
|
||||||
@@ -158,6 +174,10 @@ const emit = defineEmits<{
|
|||||||
closeMobileSidebar: [];
|
closeMobileSidebar: [];
|
||||||
toggleTheme: [];
|
toggleTheme: [];
|
||||||
toggleFullscreen: [];
|
toggleFullscreen: [];
|
||||||
|
selectProject: [projectId: string];
|
||||||
|
createProject: [];
|
||||||
|
editProject: [project: Project];
|
||||||
|
deleteProject: [projectId: string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<v-dialog v-model="isOpen" max-width="500" @update:model-value="handleDialogChange">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="dialog-title">
|
||||||
|
{{ isEditing ? tm('project.edit') : tm('project.create') }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field v-model="form.emoji" :label="tm('project.emoji')" flat variant="solo-filled" hide-details class="mb-3" />
|
||||||
|
<v-text-field v-model="form.title" :label="tm('project.name')" flat variant="solo-filled" hide-details class="mb-3" autofocus
|
||||||
|
@keyup.enter="handleSave" />
|
||||||
|
<v-textarea v-model="form.description" :label="tm('project.description')" flat variant="solo-filled" hide-details rows="3" rounded="lg" />
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn variant="text" @click="handleCancel" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
|
||||||
|
<v-btn variant="text" @click="handleSave" color="primary" :disabled="!form.title.trim()">{{ t('core.common.save') }}</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
project_id: string;
|
||||||
|
title: string;
|
||||||
|
emoji?: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectFormData {
|
||||||
|
emoji: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean;
|
||||||
|
project?: Project | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
modelValue: false,
|
||||||
|
project: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: boolean];
|
||||||
|
save: [formData: ProjectFormData, projectId?: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const { tm } = useModuleI18n('features/chat');
|
||||||
|
|
||||||
|
const isOpen = ref(props.modelValue);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const form = ref<ProjectFormData>({
|
||||||
|
emoji: '📁',
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
isOpen.value = newVal;
|
||||||
|
if (newVal) {
|
||||||
|
// 打开对话框时初始化表单
|
||||||
|
if (props.project) {
|
||||||
|
isEditing.value = true;
|
||||||
|
form.value = {
|
||||||
|
emoji: props.project.emoji || '📁',
|
||||||
|
title: props.project.title,
|
||||||
|
description: props.project.description || ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
isEditing.value = false;
|
||||||
|
form.value = {
|
||||||
|
emoji: '📁',
|
||||||
|
title: '',
|
||||||
|
description: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDialogChange(value: boolean) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
isOpen.value = false;
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!form.value.title.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('save', { ...form.value }, props.project?.project_id);
|
||||||
|
isOpen.value = false;
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 项目按钮 -->
|
||||||
|
<div style="padding: 0 8px 0px 8px; opacity: 0.6;">
|
||||||
|
<v-btn block variant="text" class="project-btn" @click="toggleExpanded" prepend-icon="mdi-folder-outline">
|
||||||
|
{{ tm('project.title') }}
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-icon size="small">{{ expanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目列表 -->
|
||||||
|
<v-expand-transition>
|
||||||
|
<div v-show="expanded" style="padding: 0 8px;">
|
||||||
|
<v-list density="compact" nav class="project-list" style="background-color: transparent;">
|
||||||
|
<v-list-item v-for="project in projects" :key="project.project_id"
|
||||||
|
@click="$emit('selectProject', project.project_id)" rounded="lg" class="project-item">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<span class="project-emoji">{{ project.emoji || '📁' }}</span>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title class="project-title">{{ project.title }}</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="project-actions">
|
||||||
|
<v-btn icon="mdi-pencil" size="x-small" variant="text" class="edit-project-btn"
|
||||||
|
@click.stop="$emit('editProject', project)" />
|
||||||
|
<v-btn icon="mdi-delete" size="x-small" variant="text" class="delete-project-btn"
|
||||||
|
color="error" @click.stop="handleDeleteProject(project)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="$emit('createProject')" class="create-project-item" rounded="lg">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon size="small">mdi-plus</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title style="font-size: 13px;">{{ tm('project.create') }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
project_id: string;
|
||||||
|
title: string;
|
||||||
|
emoji?: string;
|
||||||
|
description?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projects: Project[];
|
||||||
|
initialExpanded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
initialExpanded: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selectProject: [projectId: string];
|
||||||
|
createProject: [];
|
||||||
|
editProject: [project: Project];
|
||||||
|
deleteProject: [projectId: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { tm } = useModuleI18n('features/chat');
|
||||||
|
|
||||||
|
const expanded = ref(props.initialExpanded);
|
||||||
|
|
||||||
|
// 从 localStorage 读取项目展开状态
|
||||||
|
const savedProjectsExpandedState = localStorage.getItem('projectsExpanded');
|
||||||
|
if (savedProjectsExpandedState !== null) {
|
||||||
|
expanded.value = JSON.parse(savedProjectsExpandedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpanded() {
|
||||||
|
expanded.value = !expanded.value;
|
||||||
|
localStorage.setItem('projectsExpanded', JSON.stringify(expanded.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteProject(project: Project) {
|
||||||
|
const message = tm('project.confirmDelete', { title: project.title });
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
emit('deleteProject', project.project_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.project-btn {
|
||||||
|
justify-content: flex-start;
|
||||||
|
background-color: transparent !important;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-item {
|
||||||
|
border-radius: 16px !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-item:hover {
|
||||||
|
background-color: rgba(103, 58, 183, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-item:hover .project-actions {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-emoji {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-project-btn,
|
||||||
|
.delete-project-btn {
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-project-btn:hover,
|
||||||
|
.delete-project-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-project-item {
|
||||||
|
border-radius: 16px !important;
|
||||||
|
padding: 4px 12px !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-project-item:hover {
|
||||||
|
background-color: rgba(103, 58, 183, 0.08);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<div class="project-sessions-container fade-in">
|
||||||
|
<div class="project-header">
|
||||||
|
<div class="project-header-info">
|
||||||
|
<span class="project-header-emoji">{{ project?.emoji || '📁' }}</span>
|
||||||
|
<h2 class="project-header-title">{{ project?.title }}</h2>
|
||||||
|
</div>
|
||||||
|
<p class="project-header-description" v-if="project?.description">
|
||||||
|
{{ project.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-input-slot">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-card flat class="project-sessions-list">
|
||||||
|
<v-list v-if="sessions.length > 0">
|
||||||
|
<v-list-item v-for="session in sessions" :key="session.session_id"
|
||||||
|
@click="$emit('selectSession', session.session_id)" class="project-session-item" rounded="lg">
|
||||||
|
<v-list-item-title>
|
||||||
|
{{ session.display_name || tm('conversation.newConversation') }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>
|
||||||
|
{{ formatDate(session.updated_at) }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="session-actions">
|
||||||
|
<v-btn icon="mdi-pencil" size="x-small" variant="text"
|
||||||
|
class="edit-session-btn"
|
||||||
|
@click.stop="$emit('editSessionTitle', session.session_id, session.display_name ?? '')" />
|
||||||
|
<v-btn icon="mdi-delete" size="x-small" variant="text"
|
||||||
|
class="delete-session-btn" color="error"
|
||||||
|
@click.stop="handleDeleteSession(session)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
<div v-else class="no-sessions-in-project">
|
||||||
|
<v-icon icon="mdi-message-off-outline" size="large" color="grey-lighten-1"></v-icon>
|
||||||
|
<p>{{ tm('project.noSessions') }}</p>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useModuleI18n } from '@/i18n/composables';
|
||||||
|
import type { Project } from '@/components/chat/ProjectList.vue';
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
session_id: string;
|
||||||
|
display_name?: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
project?: Project | null;
|
||||||
|
sessions: Session[];
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
selectSession: [sessionId: string];
|
||||||
|
editSessionTitle: [sessionId: string, title: string];
|
||||||
|
deleteSession: [sessionId: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { tm } = useModuleI18n('features/chat');
|
||||||
|
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
return new Date(dateString).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteSession(session: Session) {
|
||||||
|
const sessionTitle = session.display_name || tm('conversation.newConversation');
|
||||||
|
const message = tm('conversation.confirmDelete', { name: sessionTitle });
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
emit('deleteSession', session.session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.project-sessions-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header-emoji {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--v-theme-secondaryText);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-input-slot {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-sessions-list {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 680px;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-session-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-session-item:hover {
|
||||||
|
background-color: rgba(103, 58, 183, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-session-item:hover .session-actions {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-sessions-in-project {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 48px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-sessions-in-project p {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-menu v-model="menuOpen" :close-on-content-click="false" location="top" @update:model-value="handleMenuToggle">
|
<v-menu v-model="menuOpen" :close-on-content-click="false" location="top" @update:model-value="handleMenuToggle">
|
||||||
<template v-slot:activator="{ props: menuProps }">
|
<template v-slot:activator="{ props: menuProps }">
|
||||||
<v-chip v-bind="menuProps" class="text-none provider-chip" variant="tonal" size="x-small">
|
<v-chip v-bind="menuProps" class="text-none provider-chip" variant="tonal" :size="chipSize">
|
||||||
<v-icon start size="14">mdi-creation</v-icon>
|
<v-icon start size="14">mdi-creation</v-icon>
|
||||||
<span v-if="selectedProviderId">
|
<span v-if="selectedProviderId">
|
||||||
{{ selectedProviderId }}
|
{{ selectedProviderId }}
|
||||||
@@ -59,6 +59,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useDisplay } from 'vuetify';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
interface ModelMetadata {
|
interface ModelMetadata {
|
||||||
@@ -75,11 +76,15 @@ interface ProviderConfig {
|
|||||||
enable?: boolean;
|
enable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { mobile } = useDisplay();
|
||||||
|
|
||||||
const providerConfigs = ref<ProviderConfig[]>([]);
|
const providerConfigs = ref<ProviderConfig[]>([]);
|
||||||
const selectedProviderId = ref('');
|
const selectedProviderId = ref('');
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const menuOpen = ref(false);
|
const menuOpen = ref(false);
|
||||||
|
|
||||||
|
const chipSize = computed(() => mobile.value ? 'x-small' : 'small');
|
||||||
|
|
||||||
const filteredProviders = computed(() => {
|
const filteredProviders = computed(() => {
|
||||||
if (!searchQuery.value) {
|
if (!searchQuery.value) {
|
||||||
return providerConfigs.value;
|
return providerConfigs.value;
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome-container fade-in">
|
||||||
|
<div v-if="isLoading" class="loading-overlay-welcome">
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="48"
|
||||||
|
width="4"
|
||||||
|
color="primary"
|
||||||
|
></v-progress-circular>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="welcome-content">
|
||||||
|
<div class="welcome-title">
|
||||||
|
<span class="bot-name-container">
|
||||||
|
<span class="bot-name-text">
|
||||||
|
Hello, I'm <span class="highlight-name">AstrBot</span>
|
||||||
|
</span>
|
||||||
|
<span class="bot-name-star">⭐</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-input">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content {
|
||||||
|
padding: 24px 0px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-input {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay-welcome {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-name-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-name {
|
||||||
|
color: var(--v-theme-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-name-text {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
animation: revealText 1.2s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-name-star {
|
||||||
|
margin-left: 0;
|
||||||
|
display: inline-block;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: rotateStar 1.2s cubic-bezier(0.34, 1, 0.64, 1) forwards;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes revealText {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 9.2em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotateStar {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.welcome-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -82,6 +82,9 @@ export function useMessages(
|
|||||||
const activeSSECount = ref(0);
|
const activeSSECount = ref(0);
|
||||||
const enableStreaming = ref(true);
|
const enableStreaming = ref(true);
|
||||||
const attachmentCache = new Map<string, string>(); // attachment_id -> blob URL
|
const attachmentCache = new Map<string, string>(); // attachment_id -> blob URL
|
||||||
|
|
||||||
|
// 当前会话的项目信息
|
||||||
|
const currentSessionProject = ref<{ project_id: string; title: string; emoji: string } | null>(null);
|
||||||
|
|
||||||
// 从 localStorage 读取流式响应开关状态
|
// 从 localStorage 读取流式响应开关状态
|
||||||
const savedStreamingState = localStorage.getItem('enableStreaming');
|
const savedStreamingState = localStorage.getItem('enableStreaming');
|
||||||
@@ -179,6 +182,9 @@ export function useMessages(
|
|||||||
const response = await axios.get('/api/chat/get_session?session_id=' + sessionId);
|
const response = await axios.get('/api/chat/get_session?session_id=' + sessionId);
|
||||||
isConvRunning.value = response.data.data.is_running || false;
|
isConvRunning.value = response.data.data.is_running || false;
|
||||||
let history = response.data.data.history;
|
let history = response.data.data.history;
|
||||||
|
|
||||||
|
// 保存项目信息(如果存在)
|
||||||
|
currentSessionProject.value = response.data.data.project || null;
|
||||||
|
|
||||||
if (isConvRunning.value) {
|
if (isConvRunning.value) {
|
||||||
if (!isToastedRunningInfo.value) {
|
if (!isToastedRunningInfo.value) {
|
||||||
@@ -579,6 +585,7 @@ export function useMessages(
|
|||||||
isStreaming,
|
isStreaming,
|
||||||
isConvRunning,
|
isConvRunning,
|
||||||
enableStreaming,
|
enableStreaming,
|
||||||
|
currentSessionProject,
|
||||||
getSessionMessages,
|
getSessionMessages,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
toggleStreaming,
|
toggleStreaming,
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { Project } from '@/components/chat/ProjectList.vue';
|
||||||
|
|
||||||
|
export function useProjects() {
|
||||||
|
const projects = ref<Project[]>([]);
|
||||||
|
const selectedProjectId = ref<string | null>(null);
|
||||||
|
|
||||||
|
async function getProjects() {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/chatui_project/list');
|
||||||
|
if (res.data.status === 'ok') {
|
||||||
|
projects.value = res.data.data || [];
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch projects:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProject(title: string, emoji?: string, description?: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/chatui_project/create', {
|
||||||
|
title,
|
||||||
|
emoji: emoji || '📁',
|
||||||
|
description
|
||||||
|
});
|
||||||
|
if (res.data.status === 'ok') {
|
||||||
|
await getProjects();
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create project:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProject(projectId: string, title?: string, emoji?: string, description?: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/chatui_project/update', {
|
||||||
|
project_id: projectId,
|
||||||
|
title,
|
||||||
|
emoji,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
if (res.data.status === 'ok') {
|
||||||
|
await getProjects();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update project:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProject(projectId: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/chatui_project/delete', {
|
||||||
|
params: { project_id: projectId }
|
||||||
|
});
|
||||||
|
if (res.data.status === 'ok') {
|
||||||
|
await getProjects();
|
||||||
|
if (selectedProjectId.value === projectId) {
|
||||||
|
selectedProjectId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete project:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSessionToProject(sessionId: string, projectId: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/chatui_project/add_session', {
|
||||||
|
session_id: sessionId,
|
||||||
|
project_id: projectId
|
||||||
|
});
|
||||||
|
return res.data.status === 'ok';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add session to project:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSessionFromProject(sessionId: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/chatui_project/remove_session', {
|
||||||
|
session_id: sessionId
|
||||||
|
});
|
||||||
|
return res.data.status === 'ok';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove session from project:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProjectSessions(projectId: string) {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/chatui_project/get_sessions', {
|
||||||
|
params: { project_id: projectId }
|
||||||
|
});
|
||||||
|
if (res.data.status === 'ok') {
|
||||||
|
return res.data.data || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch project sessions:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
|
selectedProjectId,
|
||||||
|
getProjects,
|
||||||
|
createProject,
|
||||||
|
updateProject,
|
||||||
|
deleteProject,
|
||||||
|
addSessionToProject,
|
||||||
|
removeSessionFromProject,
|
||||||
|
getProjectSessions
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -70,14 +70,25 @@
|
|||||||
"disabled": "Streaming disabled",
|
"disabled": "Streaming disabled",
|
||||||
"on": "Stream",
|
"on": "Stream",
|
||||||
"off": "Normal"
|
"off": "Normal"
|
||||||
},
|
}, "config": {
|
||||||
"reasoning": {
|
"title": "Config"
|
||||||
|
}, "reasoning": {
|
||||||
"thinking": "Thinking Process"
|
"thinking": "Thinking Process"
|
||||||
},
|
},
|
||||||
"reply": {
|
"reply": {
|
||||||
"replyTo": "Reply to",
|
"replyTo": "Reply to",
|
||||||
"notFound": "Message not found"
|
"notFound": "Message not found"
|
||||||
},
|
},
|
||||||
|
"project": {
|
||||||
|
"title": "Projects",
|
||||||
|
"create": "Create Project",
|
||||||
|
"edit": "Edit Project",
|
||||||
|
"name": "Project Name",
|
||||||
|
"emoji": "Icon (Emoji)",
|
||||||
|
"description": "Description (Optional)",
|
||||||
|
"noSessions": "No conversations in this project",
|
||||||
|
"confirmDelete": "Are you sure you want to delete project \"{title}\"? Conversations in this project will not be deleted."
|
||||||
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"yesterday": "Yesterday"
|
"yesterday": "Yesterday"
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"on": "流式",
|
"on": "流式",
|
||||||
"off": "普通"
|
"off": "普通"
|
||||||
},
|
},
|
||||||
|
"config": {
|
||||||
|
"title": "配置文件"
|
||||||
|
},
|
||||||
"reasoning": {
|
"reasoning": {
|
||||||
"thinking": "思考过程"
|
"thinking": "思考过程"
|
||||||
},
|
},
|
||||||
@@ -78,6 +81,16 @@
|
|||||||
"replyTo": "引用",
|
"replyTo": "引用",
|
||||||
"notFound": "无法定位消息"
|
"notFound": "无法定位消息"
|
||||||
},
|
},
|
||||||
|
"project": {
|
||||||
|
"title": "项目",
|
||||||
|
"create": "创建项目",
|
||||||
|
"edit": "编辑项目",
|
||||||
|
"name": "项目名称",
|
||||||
|
"emoji": "图标 (Emoji)",
|
||||||
|
"description": "项目描述(可选)",
|
||||||
|
"noSessions": "该项目暂无对话",
|
||||||
|
"confirmDelete": "确定要删除项目 \"{title}\" 吗?项目中的对话不会被删除。"
|
||||||
|
},
|
||||||
"time": {
|
"time": {
|
||||||
"today": "今天",
|
"today": "今天",
|
||||||
"yesterday": "昨天"
|
"yesterday": "昨天"
|
||||||
|
|||||||
Reference in New Issue
Block a user