Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 50 additions & 23 deletions app/apps/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { use, useState, useEffect } from "react";
import { useTranslations } from "next-intl";
import { triggerLabel } from "@/lib/appTrigger";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -49,6 +50,7 @@ import { useInvalidateAppSkillDiagnostics } from "@/hooks/useAppSkillDiagnostics
import AppSkillsSection from "../components/AppSkillsSection";
import AppSkillDiagnostics from "../components/AppSkillDiagnostics";
import AgentMdEditor from "../components/AgentMdEditor";
import AppAvatar from "../components/AppAvatar";
import ReviewLogDialog from "@/components/ReviewLogDialog";
import ReviewRejectDialog from "@/components/ReviewRejectDialog";
import { useCurrentUser } from "@/hooks/useCurrentUser";
Expand Down Expand Up @@ -85,6 +87,8 @@ interface AppInfo {
app_type: string;
platform: string;
ai_model: string;
/** 头像 URL:内置头像静态路径或上传后的 OSS 读代理路径;空=按名称生成占位 */
avatar_url?: string | null;
agent_md?: string | null;
/** legacy 提示词绑定;用来判断 Agent.md 区块该说「升级」还是「创建」 */
prompt_id?: number | null;
Expand All @@ -96,6 +100,8 @@ interface AppInfo {
visibility?: string;
/** 创建者(提交人)用户ID,自审判定用 */
user_id?: number | null;
/** 作者显示名(nickname 优先退 username)。用户被删除时为空 */
author?: string | null;
owner_dept_id?: number | null;
owner_tenant_id?: number | null;
}
Expand Down Expand Up @@ -245,10 +251,10 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
// 审核人不能审自己提交的对象(超管除外,后端违者 403)
const selfReview = isSelfReview(user?.id, appInfo.user_id, checkSuperAdmin(user));
// 状态徽标并入基本信息首行;动作/提示另起一行,且只在真有内容时渲染
// 提交审核按钮已移到标题右侧,草稿态下这条 footer 便没有内容了——留着就是一条
// 空的分隔线。rejected 仍要留:那里有"查看驳回理由"的链接,塞不进 hover。
const reviewFooterVisible =
status === "draft" ||
status === "rejected" ||
(status === "pending_review" && canReview);
status === "rejected" || (status === "pending_review" && canReview);

return (
<div
Expand All @@ -267,6 +273,11 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
<ArrowLeft className="h-4 w-4 mr-2" />
{t("back")}
</Button>
<AppAvatar
src={appInfo.avatar_url}
name={appInfo.name}
size={isCustom ? 32 : 44}
/>
<div>
<h1 className={isCustom ? "text-xl font-bold leading-tight" : "text-3xl font-bold"}>
{appInfo.name}
Expand All @@ -281,12 +292,31 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
{appInfo.app_type !== "Custom" && (
<Card>
<CardHeader>
<CardTitle>{t("basicInfo")}</CardTitle>
{/* 动作按钮放标题右侧,与「已绑定工具」「已绑定 Skill」两区一致。
原先它单独占一行、下面还跟一句说明,把一屏里最靠上的位置让给了
一个大多数时候不需要点的按钮;说明文字改挂到按钮的 hover 上。 */}
<div className="flex items-center justify-between gap-2">
<CardTitle>{t("basicInfo")}</CardTitle>
{(status === "draft" || status === "rejected") && (
<Button
size="sm"
variant="outline"
onClick={handleSubmitReview}
disabled={reviewActionPending}
title={status === "draft" ? tr("draftOwnerOnlyHint") : undefined}
>
<Send className="h-4 w-4 mr-1" />
{ts("submitReview")}
</Button>
)}
</div>
</CardHeader>
<CardContent className="grid grid-cols-2 md:grid-cols-5 gap-4">
{/* 6 列:触发方式 / 平台 / AI 模型 / 作者 / 创建时间 / 状态。
加了作者之后是 6 格,留在 5 列会让最后一格独占一行。 */}
<CardContent className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div>
<div className="text-sm text-muted-foreground mb-1">{t("appType")}</div>
<Badge variant="outline">{appInfo.app_type}</Badge>
<Badge variant="outline">{triggerLabel(appInfo.app_type, t)}</Badge>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1">{t("platform")}</div>
Expand All @@ -296,6 +326,12 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
<div className="text-sm text-muted-foreground mb-1">{t("aiModel")}</div>
<div className="text-sm">{appInfo.ai_model}</div>
</div>
{/* 作者。用户被删除时后端给 null——显示 — 而不是整格消失:
这是固定列的网格,少一格会让后面的列错位。 */}
<div>
<div className="text-sm text-muted-foreground mb-1">{t("author")}</div>
<div className="text-sm">{appInfo.author || "—"}</div>
</div>
<div>
<div className="text-sm text-muted-foreground mb-1">{tc("createdAt")}</div>
<div className="text-sm">{formatDate(appInfo.created_at)}</div>
Expand All @@ -310,19 +346,8 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
)}
{/* 审核动作与状态提示:仅在确有内容时才占一行,已发布应用不留空带 */}
{reviewFooterVisible && (
<div className="col-span-2 md:col-span-5 pt-2 border-t">
<div className="col-span-2 md:col-span-3 lg:col-span-6 pt-2 border-t">
<div className="flex items-center gap-2 flex-wrap">
{(status === "draft" || status === "rejected") && (
<Button
size="sm"
variant="outline"
onClick={handleSubmitReview}
disabled={reviewActionPending}
>
<Send className="h-4 w-4 mr-1" />
{ts("submitReview")}
</Button>
)}
{status === "pending_review" &&
canReview &&
(selfReview ? (
Expand Down Expand Up @@ -361,9 +386,6 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
</Button>
</p>
)}
{status === "draft" && (
<p className="text-xs text-muted-foreground mt-2">{tr("draftOwnerOnlyHint")}</p>
)}
</div>
)}
</CardContent>
Expand Down Expand Up @@ -392,8 +414,13 @@ export default function AppDetailPage({ params }: { params: Promise<{ id: string
{/* 以下工具统计 / 绑定列表仅非 Custom 应用展示 */}
{appInfo.app_type !== "Custom" && (
<>
{/* 工具统计 */}
{statistics && (
{/*
工具统计。没绑工具就整行不显示——三个 0 既不提供信息,还占掉一屏顶部
最显眼的位置。这么判是安全的:后端的统计就是遍历 app_tools 绑定关系算
出来的(tools.py get_app_tools_statistics),零绑定时三个数必然都是 0,
不存在"有历史调用但被藏起来"的情况。
*/}
{statistics && appTools.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-3">
Expand Down
66 changes: 8 additions & 58 deletions app/apps/components/AgentMdEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
Expand All @@ -13,24 +12,13 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
AlertCircle,
AlertTriangle,
Eye,
FileText,
Loader2,
Sparkles,
Undo2,
X,
} from "lucide-react";
import { useAgentMd, type AgentMdValidationError } from "@/hooks/useAgentMd";
Expand Down Expand Up @@ -62,7 +50,7 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
const { user } = useCurrentUser();
const canManage = checkSuperAdmin(user);

const { agentMd, loading, save, generate, remove, fetchExport } = useAgentMd(appId);
const { agentMd, loading, save, generate, fetchExport } = useAgentMd(appId);

const [content, setContent] = useState("");
const [dirty, setDirty] = useState(false);
Expand All @@ -72,7 +60,6 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
const [saveWarnings, setSaveWarnings] = useState<string[]>([]);
const [exportContent, setExportContent] = useState<string | null>(null);
const [exportLoading, setExportLoading] = useState(false);
const [revertDialogOpen, setRevertDialogOpen] = useState(false);

const isWechat = platform === "Wechat";
const isLegacy = agentMd?.is_legacy !== false;
Expand Down Expand Up @@ -114,17 +101,6 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
}
};

const handleRevert = async () => {
const ok = await remove();
if (ok) {
setRevertDialogOpen(false);
setDirty(false);
setContent("");
setValidationErrors([]);
setSaveWarnings([]);
onChanged?.();
}
};

const handleExportView = async () => {
setExportLoading(true);
Expand Down Expand Up @@ -157,14 +133,15 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
<Card>
<CardHeader>
<div className="flex items-center justify-between">
{/*
标题直接写「角色设定」,不再显示文件名 Agent.md,也不再挂状态徽章:
标题旁边写着「角色设定」、下面又挂一个写着「角色设定」的彩色 Badge,
是同一句话说两遍。isLegacy 的状态由下面那段说明文字承担
(legacyDesc / noAgentMdDesc 已经把处境讲清楚了)。
*/}
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Agent.md
{isLegacy ? (
<Badge variant="secondary">{t("legacyMode")}</Badge>
) : (
<Badge>{t("agentMdMode")}</Badge>
)}
{t("agentMdMode")}
</CardTitle>
{!isLegacy && canManage && (
<div className="flex gap-2">
Expand All @@ -181,15 +158,6 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
)}
{t("exportView")}
</Button>
<Button
variant="outline"
size="sm"
className="text-destructive"
onClick={() => setRevertDialogOpen(true)}
>
<Undo2 className="h-4 w-4 mr-2" />
{t("revertToPrompt")}
</Button>
<Button size="sm" onClick={handleSave} disabled={saving || !dirty}>
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
{tc("save")}
Expand Down Expand Up @@ -313,24 +281,6 @@ export default function AgentMdEditor({ appId, platform, promptId, onChanged }:
</DialogContent>
</Dialog>

{/* 回退到提示词:二次确认 */}
<AlertDialog open={revertDialogOpen} onOpenChange={setRevertDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirmRevert")}</AlertDialogTitle>
<AlertDialogDescription>{t("revertWarning")}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{tc("cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={handleRevert}
className="bg-destructive text-destructive-foreground"
>
{t("revertToPrompt")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
);
}
54 changes: 54 additions & 0 deletions app/apps/components/AppAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client";

import { avatarFallbackText } from "@/lib/appAvatar";

interface Props {
/** 头像 URL;空 = 未设置,显示按名称生成的占位 */
src?: string | null;
name: string;
/** 边长(px)。占位文字按边长取比例,保证各尺寸下观感一致 */
size?: number;
className?: string;
}

/**
* 数字员工头像。列表、卡片、详情页、编辑弹窗共用一个,样式规则就不会四处各写一遍
* 然后慢慢走样——同一个员工在不同页面显示成不同样子是很难察觉却很伤的那种不一致。
*
* ## 颜色归属
*
* · **选了头像**(内置的或上传的)→ 原样渲染,用它自己的颜色。用户明确挑过的东西
* 不该被主题覆盖,内置那八张各有一套配色,挑哪张就是哪个颜色。
* · **没选头像** → 名称首字 + **品牌主色**底(`bg-primary/10` / `text-primary`)。
* 这是系统替他填的空白,就该跟着品牌走,而不是由我们随便指定一个颜色;租户改了
* 主色它会跟着变。存量应用全是这一种。
*
* 所以两条分支的判据是"有没有 src",而不是"内置还是上传"——只要是他选的,一律不动色。
*/
export default function AppAvatar({ src, name, size = 40, className = "" }: Props) {
const box = { width: size, height: size };
// 圆角跟着尺寸走:固定 rounded-lg 在 24px 上显得太圆、在 64px 上显得太方
const radius = Math.round(size * 0.28);

if (src) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={src}
alt=""
style={{ ...box, borderRadius: radius }}
className={`object-cover bg-muted shrink-0 ${className}`}
/>
);
}

return (
<div
style={{ ...box, borderRadius: radius, fontSize: Math.round(size * 0.45) }}
className={`flex items-center justify-center shrink-0 font-semibold bg-primary/10 text-primary ${className}`}
aria-hidden
>
{avatarFallbackText(name)}
</div>
);
}
Loading
Loading