diff --git a/backend/alembic/versions/8fe654655905_004_add_aimodel_auto.py b/backend/alembic/versions/8fe654655905_004_add_aimodel_auto.py new file mode 100644 index 00000000..35b05838 --- /dev/null +++ b/backend/alembic/versions/8fe654655905_004_add_aimodel_auto.py @@ -0,0 +1,43 @@ +"""004_add_aimodel_auto + +Revision ID: 8fe654655905 +Revises: d116056121c3 +Create Date: 2025-05-07 15:51:34.768842 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '8fe654655905' +down_revision = 'd116056121c3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('ai_model', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('type', sa.Integer(), nullable=False), + sa.Column('api_key', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('endpoint', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False), + sa.Column('max_context_window', sa.Integer(), nullable=False), + sa.Column('temperature', sa.Float(), nullable=False), + sa.Column('status', sa.Boolean(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True), + sa.Column('create_time', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_ai_model_id'), 'ai_model', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_ai_model_id'), table_name='ai_model') + op.drop_table('ai_model') + # ### end Alembic commands ### diff --git a/backend/apps/api.py b/backend/apps/api.py index 0c0d312b..478d86b3 100644 --- a/backend/apps/api.py +++ b/backend/apps/api.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from apps.system.api import login, user +from apps.system.api import login, user, aimodel from apps.settings.api import terminology from apps.datasource.api import datasource @@ -8,6 +8,7 @@ from apps.datasource.api import datasource api_router = APIRouter() api_router.include_router(login.router) api_router.include_router(user.router) +api_router.include_router(aimodel.router) api_router.include_router(terminology.router) api_router.include_router(datasource.router) diff --git a/backend/apps/system/api/aimodel.py b/backend/apps/system/api/aimodel.py new file mode 100644 index 00000000..a3284125 --- /dev/null +++ b/backend/apps/system/api/aimodel.py @@ -0,0 +1,86 @@ +from datetime import datetime +from fastapi import APIRouter +from sqlmodel import select +from apps.system.models.system_modle import AiModelDetail +from apps.system.schemas.system_schema import model_status +from common.core.deps import SessionDep +from common.core.pagination import Paginator +from common.core.schemas import PaginatedResponse, PaginationParams +from common.utils.time import get_timestamp + +router = APIRouter(tags=["system/aimodel"], prefix="/system/aimodel") + + +@router.get("/pager/{pageNum}/{pageSize}", response_model=PaginatedResponse[AiModelDetail]) +async def pager( + session: SessionDep, + pageNum: int, + pageSize: int +): + pagination = PaginationParams(page=pageNum, size=pageSize) + paginator = Paginator(session) + filters = {} + return await paginator.get_paginated_response( + model=AiModelDetail, + pagination=pagination, + **filters) + +@router.get("/{id}", response_model=AiModelDetail) +async def get_model_by_id( + session: SessionDep, + id: int +): + term = session.get(AiModelDetail, id) + return term + +@router.post("", response_model=AiModelDetail) +async def add_model( + session: SessionDep, + creator: AiModelDetail +): + data = AiModelDetail.model_validate(creator) + data.create_time = get_timestamp() + session.add(data) + session.commit() + return creator + +@router.put("", response_model=AiModelDetail) +async def update_terminology( + session: SessionDep, + model: AiModelDetail +): + model.id = int(model.id) + term = session.exec(select(AiModelDetail).where(AiModelDetail.id == model.id)).first() + update_data = model.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(term, field, value) + session.add(term) + session.commit() + return model + +@router.delete("/{id}", response_model=AiModelDetail) +async def delete_terminology( + session: SessionDep, + id: int +): + term = session.exec(select(AiModelDetail).where(AiModelDetail.id == id)).first() + session.delete(term) + session.commit() + return { + "message": f"AiModel with ID {id} deleted successfully." + } + + +@router.patch("/status", response_model=dict) +async def status(session: SessionDep, dto: model_status): + ids = dto.ids + status = dto.status + if not ids: + return {"message": "ids is empty"} + statement = select(AiModelDetail).where(AiModelDetail.id.in_(ids)) + terms = session.exec(statement).all() + for term in terms: + term.status = status + session.add(term) + session.commit() + return {"message": f"AiModel with IDs {ids} updated successfully."} \ No newline at end of file diff --git a/backend/apps/system/models/system_modle.py b/backend/apps/system/models/system_modle.py new file mode 100644 index 00000000..cc35045e --- /dev/null +++ b/backend/apps/system/models/system_modle.py @@ -0,0 +1,18 @@ + +from sqlmodel import BigInteger, Field, SQLModel +from common.core.models import SnowflakeBase + + +class AiModelBase: + name: str = Field(max_length=255, nullable=False) + type: int = Field(nullable=False) + +class AiModelDetail(AiModelBase, SnowflakeBase, table=True): + __tablename__ = "ai_model" + api_key: str | None = Field(max_length=255, nullable=True) + endpoint: str = Field(max_length=255, nullable=False) + max_context_window: int = Field(default=0) + temperature: float = Field(default=0.0) + status: bool = Field(default=True) + description: str | None = Field(max_length=255, nullable=True) + create_time: int = Field(default=0, sa_type=BigInteger()) \ No newline at end of file diff --git a/backend/apps/system/schemas/system_schema.py b/backend/apps/system/schemas/system_schema.py new file mode 100644 index 00000000..237612cd --- /dev/null +++ b/backend/apps/system/schemas/system_schema.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class model_status(BaseModel): + status: bool + ids: list[int] \ No newline at end of file diff --git a/backend/common/core/models.py b/backend/common/core/models.py index 57718314..d6488f59 100644 --- a/backend/common/core/models.py +++ b/backend/common/core/models.py @@ -13,21 +13,7 @@ class SnowflakeBase(SQLModel): nullable=False ) - @field_validator("id", mode="before") - def validate_id(cls, v): - if isinstance(v, str): - if not v: - return None - try: - return int(v) - except ValueError: - raise ValueError("Invalid bigint string") - elif isinstance(v, int): - return v - raise TypeError("BigInt must be int or string") - - class Config: json_encoders = { - int: lambda v: str(v) if v > 2**53-1 else v + int: lambda v: str(v) if isinstance(v, int) and v > (2**53 - 1) else v } \ No newline at end of file diff --git a/backend/common/utils/time.py b/backend/common/utils/time.py new file mode 100644 index 00000000..7612c5f2 --- /dev/null +++ b/backend/common/utils/time.py @@ -0,0 +1,6 @@ +from datetime import datetime + + +def get_timestamp(): + dt_millis = int(datetime.now().timestamp() * 1000) + return dt_millis \ No newline at end of file diff --git a/backend/scripts/alembic/auto.sh b/backend/scripts/alembic/auto.sh new file mode 100755 index 00000000..d94dfd21 --- /dev/null +++ b/backend/scripts/alembic/auto.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +usage() { + echo -e "${GREEN}Usage:${NC} $0 [\"migration message\"]" + echo -e "Example: $0 \"Added user table\"" + exit 1 +} + +if [ "$#" -eq 0 ]; then + echo -e "${RED}Error:${NC} No migration message provided" + usage +fi + +echo -e "${GREEN}Generating migration with message:${NC} \"$1\"" +alembic revision --autogenerate -m "$1" + +if [ $? -eq 0 ]; then + echo -e "${GREEN}Migration created successfully!${NC}" +else + echo -e "${RED}Error:${NC} Failed to create migration" + exit 1 +fi \ No newline at end of file diff --git a/frontend/components.d.ts b/frontend/components.d.ts index dc9c610f..706ada35 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -11,6 +11,7 @@ declare module 'vue' { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] + ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] @@ -22,7 +23,9 @@ declare module 'vue' { ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSlider: typeof import('element-plus/es')['ElSlider'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] diff --git a/frontend/src/api/system.ts b/frontend/src/api/system.ts new file mode 100644 index 00000000..106fa74c --- /dev/null +++ b/frontend/src/api/system.ts @@ -0,0 +1,10 @@ +import { request } from '@/utils/request' + +export const modelApi = { + pager: (pageNumber: number, pageSize: number) => request.get(`/system/aimodel/pager/${pageNumber}/${pageSize}`), + add: (data: any) => request.post('/system/aimodel', data), + edit: (data: any) => request.put('/system/aimodel', data), + delete: (id: number) => request.delete(`/system/aimodel/${id}`), + query: (id: number) => request.get(`/system/aimodel/${id}`), + status: (data: any) => request.patch('/system/aimodel/status', data), +} \ No newline at end of file diff --git a/frontend/src/assets/svg/operate/ope-disable.svg b/frontend/src/assets/svg/operate/ope-disable.svg new file mode 100644 index 00000000..7821cfb1 --- /dev/null +++ b/frontend/src/assets/svg/operate/ope-disable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/svg/operate/ope-enable.svg b/frontend/src/assets/svg/operate/ope-enable.svg new file mode 100644 index 00000000..4c4c44e6 --- /dev/null +++ b/frontend/src/assets/svg/operate/ope-enable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 5cc881f7..b7d31333 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -83,7 +83,7 @@ class HttpService { // Response interceptor this.instance.interceptors.response.use( (response: AxiosResponse) => { - console.log(`[Response] ${response.config.url}`, response.data) + // console.log(`[Response] ${response.config.url}`, response.data) // Return raw response if configured if ((response.config as FullRequestConfig).requestOptions?.rawResponse) { @@ -164,8 +164,7 @@ class HttpService { // ElMessage.error(errorMessage) ElMessage({ message: errorMessage, - type: 'error', - duration: 0 + type: 'error' }) } diff --git a/frontend/src/views/system/model/index.vue b/frontend/src/views/system/model/index.vue index ebe7f3bc..276aaec8 100644 --- a/frontend/src/views/system/model/index.vue +++ b/frontend/src/views/system/model/index.vue @@ -1,3 +1,517 @@ \ No newline at end of file +
+
+
+ + + +
+
+
+ + Batch Enable +
+
+ + Batch Disable +
+
+ + Add Model +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/views/system/user/index.vue b/frontend/src/views/system/user/index.vue index b3419d1c..60165523 100644 --- a/frontend/src/views/system/user/index.vue +++ b/frontend/src/views/system/user/index.vue @@ -63,6 +63,19 @@ + +
+ +
@@ -124,6 +137,11 @@ const state = reactive({ definition: '', domain: '' }, + pageInfo: { + currentPage: 1, + pageSize: 20, + total: 0 + } }) const handleSearch = (e: any) => { console.log('search', e); @@ -182,8 +200,9 @@ const onFormClose = () => { } const search = () => { - userApi.pager(1, 20).then((res: any) => { + userApi.pager(state.pageInfo.currentPage, state.pageInfo.pageSize).then((res: any) => { state.tableData = res.items + state.pageInfo.total = res.total }) } const addTerm = () => { @@ -214,6 +233,14 @@ const saveHandler = () => { addTerm() } } +const handleSizeChange = (val: number) => { + state.pageInfo.pageSize = val + search() +} +const handleCurrentChange = (val: number) => { + state.pageInfo.currentPage = val + search() +} onMounted(() => { search() }) @@ -228,7 +255,11 @@ onMounted(() => { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + background-color: var(--white); + padding: 16px 20px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; .tool-left { display: flex; align-items: center; @@ -295,11 +326,14 @@ onMounted(() => { td { height: 52px; } + border-left: 1px solid var(--el-table-border-color); + border-right: 1px solid var(--el-table-border-color); } .table-operate { display: flex; justify-content: flex-end; gap: 8px; + .opt-btn { width: 36px; height: 36px; @@ -319,6 +353,16 @@ onMounted(() => { } } } + .pagination-container { + display: flex; + justify-content: end; + align-items: center; + margin-top: 20px; + background-color: var(--white); + padding: 16px 20px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } } } .sqlbot-form {