mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 18:15:34 +00:00
feat(teams): task hints for weak models + force v2
- Add lead hint in search/list results with member+model info and task creation guidelines (description quality, complexity, model matching) - Add member instructions in dispatch message (progress, comment, complete) - Include member comments and attachments in announce content to leader - Cap announce content at 50k runes to prevent context blowup - Remove duplicate comment block in announce path - Force all teams to v2: remove IsTeamV2 gate, v2Actions map, isConsumerTeamV2, and v1 TEAM.md branch - Replace version comparison modal with features showcase modal - Remove version selector from team settings UI
This commit is contained in:
1 parent
57f249a89d
commit
8d3eb5bb6f
17 files changed
+259
-362
No files matched your search
@@ -136,11 +136,6 @@ func autoSetFollowup(ctx context.Context, teamStore store.TeamStore, agentStore
|
||||
if err != nil || team == nil || team.LeadAgentID != ag.ID {
|
||||
return // only lead agent triggers auto-set
|
||||
}
|
||||
// Followup is a v2 feature.
|
||||
if !isConsumerTeamV2(team) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip auto-followup when lead is waiting for teammates (not user).
|
||||
if hasMember, _ := teamStore.HasActiveMemberTasks(ctx, team.ID, ag.ID); hasMember {
|
||||
slog.Debug("auto-followup: skipping, active member tasks exist", "team_id", team.ID)
|
||||
@@ -159,9 +154,6 @@ func autoSetFollowup(ctx context.Context, teamStore store.TeamStore, agentStore
|
||||
}
|
||||
}
|
||||
|
||||
// isConsumerTeamV2 delegates to tools.IsTeamV2 for version checking.
|
||||
var isConsumerTeamV2 = tools.IsTeamV2
|
||||
|
||||
// parseFollowupSettings extracts followup interval and max reminders from team settings.
|
||||
func parseFollowupSettings(team *store.TeamData) (time.Duration, int) {
|
||||
const (
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -330,7 +331,7 @@ func handleTeammateMessage(
|
||||
teamID, _ := uuid.Parse(inMeta["team_id"])
|
||||
if teamTaskID != uuid.Nil && teamStore != nil {
|
||||
cachedTeam, _ = teamStore.GetTeam(ctx, teamID)
|
||||
if cachedTeam != nil && isConsumerTeamV2(cachedTeam) {
|
||||
if cachedTeam != nil {
|
||||
// Check current task status — agent may have already updated it via tool.
|
||||
currentTask, taskErr := teamStore.GetTask(ctx, teamTaskID)
|
||||
alreadyTerminal := taskErr == nil && currentTask != nil &&
|
||||
@@ -432,6 +433,33 @@ func handleTeammateMessage(
|
||||
announceMedia = outcome.Result.Media
|
||||
}
|
||||
|
||||
// Append member comments & attachments so leader sees them in the announce.
|
||||
if taskIDStr := inMeta["team_task_id"]; taskIDStr != "" && teamStore != nil {
|
||||
if taskUUID, err := uuid.Parse(taskIDStr); err == nil {
|
||||
if comments, err := teamStore.ListRecentTaskComments(ctx, taskUUID, 5); err == nil && len(comments) > 0 {
|
||||
var parts []string
|
||||
for _, c := range comments {
|
||||
author := c.AgentKey
|
||||
if author == "" {
|
||||
author = "system"
|
||||
}
|
||||
text := c.Content
|
||||
if len([]rune(text)) > 500 {
|
||||
text = string([]rune(text)[:500]) + "..."
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("- [%s]: %s", author, text))
|
||||
}
|
||||
announceContent += "\n\n[Member notes]\n" + strings.Join(parts, "\n")
|
||||
}
|
||||
if attachments, err := teamStore.ListTaskAttachments(ctx, taskUUID); err == nil && len(attachments) > 0 {
|
||||
announceContent += "\n\n[Attached files in team workspace]"
|
||||
for _, a := range attachments {
|
||||
announceContent += "\n- " + filepath.Base(a.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Announce result (or failure) to lead agent via announce queue.
|
||||
// Queue merges concurrent completions into a single batched announce.
|
||||
if origChatID == "" {
|
||||
@@ -483,30 +511,9 @@ func handleTeammateMessage(
|
||||
parentRootSpanID, _ = uuid.Parse(sid)
|
||||
}
|
||||
|
||||
// Enrich announce content with member comments on the task (if any).
|
||||
if taskIDStr := inMeta["team_task_id"]; taskIDStr != "" && teamStore != nil {
|
||||
if tid, err := uuid.Parse(taskIDStr); err == nil {
|
||||
if comments, err := teamStore.ListTaskComments(ctx, tid); err == nil && len(comments) > 0 {
|
||||
// Include last 5 comments, each truncated to 200 chars.
|
||||
start := 0
|
||||
if len(comments) > 5 {
|
||||
start = len(comments) - 5
|
||||
}
|
||||
var commentLines []string
|
||||
for _, c := range comments[start:] {
|
||||
author := c.AgentKey
|
||||
if author == "" && c.UserID != "" {
|
||||
author = "user:" + c.UserID
|
||||
}
|
||||
body := c.Content
|
||||
if len(body) > 200 {
|
||||
body = body[:200] + "..."
|
||||
}
|
||||
commentLines = append(commentLines, fmt.Sprintf("- [%s]: %s", author, body))
|
||||
}
|
||||
announceContent += "\n\n--- Member comments ---\n" + strings.Join(commentLines, "\n")
|
||||
}
|
||||
}
|
||||
// Cap announce content to prevent context blowup for the leader agent.
|
||||
if len([]rune(announceContent)) > 50_000 {
|
||||
announceContent = string([]rune(announceContent)[:50_000]) + "\n[truncated]"
|
||||
}
|
||||
|
||||
// Enqueue result. If we become the processor, run the announce loop.
|
||||
|
||||
@@ -162,7 +162,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
|
||||
hasTeam = true
|
||||
contextFiles = append(contextFiles, bootstrap.ContextFile{
|
||||
Path: bootstrap.TeamFile,
|
||||
Content: buildTeamMD(team, members, ag.ID, tools.IsTeamV2(team)),
|
||||
Content: buildTeamMD(team, members, ag.ID),
|
||||
})
|
||||
// Detect lead role for tool policy
|
||||
for _, m := range members {
|
||||
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
|
||||
// buildTeamMD generates compact TEAM.md content for an agent that is part of a team.
|
||||
// Kept minimal — tool descriptions already live in tool Parameters()/Description().
|
||||
// isV2 controls whether advanced sections (orchestration, followup, review) are rendered.
|
||||
func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uuid.UUID, isV2 bool) string {
|
||||
func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uuid.UUID) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Team: " + team.Name + "\n")
|
||||
if team.Description != "" {
|
||||
@@ -77,48 +76,37 @@ func buildTeamMD(team *store.TeamData, members []store.TeamMemberData, selfID uu
|
||||
// Workflow guidance — version-aware to match backend behavior.
|
||||
sb.WriteString("\n## Workflow\n\n")
|
||||
if selfRole == store.TeamRoleLead {
|
||||
if isV2 {
|
||||
sb.WriteString("Delegate work to team members using `team_tasks` with `assignee`.\n\n")
|
||||
sb.WriteString("```\nteam_tasks(action=\"create\", subject=\"...\", description=\"...\", assignee=\"agent-key\")\n```\n\n")
|
||||
sb.WriteString("The system auto-dispatches to the assigned member and auto-completes when done.\n")
|
||||
sb.WriteString("Do NOT use `spawn` for team delegation — `spawn` is only for self-clone subagent work.\n\n")
|
||||
sb.WriteString("Rules:\n")
|
||||
sb.WriteString("- Always specify `assignee` — match member expertise from the list above\n")
|
||||
sb.WriteString("- **Check task board first** — call `team_tasks(action=\"search\", query=\"<keywords>\")` to find similar tasks before creating. This uses semantic search and saves tokens vs listing all. The system blocks creation if you skip this step\n")
|
||||
sb.WriteString("- **Create ALL tasks upfront** in one batch, then announce — then STOP. Do NOT create one task, wait for it to finish, then create the next\n")
|
||||
sb.WriteString("- Delegation is NOT completion — do NOT say \"done\"/\"xong\"/\"finished\" after delegating. Only report completion when ALL task results have been delivered\n")
|
||||
sb.WriteString("- Results arrive automatically — do NOT present partial results\n")
|
||||
sb.WriteString("- **Prefer delegation** — delegate tasks to members, do NOT do the work yourself. If you need research, create a research task. If you need code, create a coding task. Only use tools like web_search yourself for quick clarifications needed to plan tasks (e.g., checking a member's capability), NOT for producing deliverables\n")
|
||||
sb.WriteString("- **Do NOT block on completed tasks** — pass completed task's result in the description instead of using blocked_by\n")
|
||||
sb.WriteString("Delegate work to team members using `team_tasks` with `assignee`.\n\n")
|
||||
sb.WriteString("```\nteam_tasks(action=\"create\", subject=\"...\", description=\"...\", assignee=\"agent-key\")\n```\n\n")
|
||||
sb.WriteString("The system auto-dispatches to the assigned member and auto-completes when done.\n")
|
||||
sb.WriteString("Do NOT use `spawn` for team delegation — `spawn` is only for self-clone subagent work.\n\n")
|
||||
sb.WriteString("Rules:\n")
|
||||
sb.WriteString("- Always specify `assignee` — match member expertise from the list above\n")
|
||||
sb.WriteString("- **Check task board first** — call `team_tasks(action=\"search\", query=\"<keywords>\")` to find similar tasks before creating. This uses semantic search and saves tokens vs listing all. The system blocks creation if you skip this step\n")
|
||||
sb.WriteString("- **Create ALL tasks upfront** in one batch, then announce — then STOP. Do NOT create one task, wait for it to finish, then create the next\n")
|
||||
sb.WriteString("- Delegation is NOT completion — do NOT say \"done\"/\"xong\"/\"finished\" after delegating. Only report completion when ALL task results have been delivered\n")
|
||||
sb.WriteString("- Results arrive automatically — do NOT present partial results\n")
|
||||
sb.WriteString("- **Prefer delegation** — delegate tasks to members, do NOT do the work yourself. If you need research, create a research task. If you need code, create a coding task. Only use tools like web_search yourself for quick clarifications needed to plan tasks (e.g., checking a member's capability), NOT for producing deliverables\n")
|
||||
sb.WriteString("- **Do NOT block on completed tasks** — pass completed task's result in the description instead of using blocked_by\n")
|
||||
|
||||
sb.WriteString("\n## Task Planning\n\n")
|
||||
sb.WriteString("**CRITICAL: Create the full task graph in ONE batch.** Do NOT create→wait→create sequentially.\n\n")
|
||||
sb.WriteString("Each task = ONE deliverable. Complex requests need 3+ tasks.\n\n")
|
||||
sb.WriteString("1. Identify ALL distinct deliverables (research, writing, design, code...)\n")
|
||||
sb.WriteString("2. Create independent tasks FIRST → get their UUIDs from the response\n")
|
||||
sb.WriteString("3. THEN create dependent tasks with `blocked_by=[UUID]` — the system auto-dispatches when blockers complete\n")
|
||||
sb.WriteString(" `blocked_by` only accepts real UUIDs returned by previous create calls. Never use placeholders.\n\n")
|
||||
sb.WriteString("Same member → sequential (higher priority first). Different members → parallel.\n\n")
|
||||
sb.WriteString("**Anti-pattern (WRONG):** create task A → wait for A to finish → create task B → wait...\n")
|
||||
sb.WriteString("**Correct pattern:** create A → create B → create C(blocked_by=[A.id, B.id]) → announce → STOP. You must create tasks first to get their UUIDs, then use those UUIDs in blocked_by of dependent tasks.\n\n")
|
||||
sb.WriteString("**Example:** User: \"research X and make infographic\"\n")
|
||||
sb.WriteString("→ WRONG: web_search(X) yourself → summarize → delegate only infographic\n")
|
||||
sb.WriteString("→ RIGHT: task 1 \"Research X\" (assignee=researcher) → task 2 \"Create infographic about X\" (assignee=artist, blocked_by=[task1.id])\n")
|
||||
sb.WriteString("\n## Task Planning\n\n")
|
||||
sb.WriteString("**CRITICAL: Create the full task graph in ONE batch.** Do NOT create→wait→create sequentially.\n\n")
|
||||
sb.WriteString("Each task = ONE deliverable. Complex requests need 3+ tasks.\n\n")
|
||||
sb.WriteString("1. Identify ALL distinct deliverables (research, writing, design, code...)\n")
|
||||
sb.WriteString("2. Create independent tasks FIRST → get their UUIDs from the response\n")
|
||||
sb.WriteString("3. THEN create dependent tasks with `blocked_by=[UUID]` — the system auto-dispatches when blockers complete\n")
|
||||
sb.WriteString(" `blocked_by` only accepts real UUIDs returned by previous create calls. Never use placeholders.\n\n")
|
||||
sb.WriteString("Same member → sequential (higher priority first). Different members → parallel.\n\n")
|
||||
sb.WriteString("**Anti-pattern (WRONG):** create task A → wait for A to finish → create task B → wait...\n")
|
||||
sb.WriteString("**Correct pattern:** create A → create B → create C(blocked_by=[A.id, B.id]) → announce → STOP. You must create tasks first to get their UUIDs, then use those UUIDs in blocked_by of dependent tasks.\n\n")
|
||||
sb.WriteString("**Example:** User: \"research X and make infographic\"\n")
|
||||
sb.WriteString("→ WRONG: web_search(X) yourself → summarize → delegate only infographic\n")
|
||||
sb.WriteString("→ RIGHT: task 1 \"Research X\" (assignee=researcher) → task 2 \"Create infographic about X\" (assignee=artist, blocked_by=[task1.id])\n")
|
||||
|
||||
sb.WriteString("\n## Follow-up Reminders\n\n")
|
||||
sb.WriteString("When you need user input/decision: create+claim task, then `ask_user` with text=<question>. ONLY use when you have a question for the user — NOT for waiting on teammates or status updates.\n")
|
||||
sb.WriteString("IMPORTANT: Present the question directly to the user in your response. `ask_user` only sets up periodic REMINDERS in case they don't reply — it does NOT present the question for you.\n")
|
||||
sb.WriteString("System auto-sends reminders. Call `clear_ask_user` when user replies.\n")
|
||||
} else {
|
||||
sb.WriteString("Create a task with `team_tasks` (with `assignee`), then the system dispatches automatically.\n")
|
||||
sb.WriteString("Tasks auto-complete when the member finishes.\n\n")
|
||||
sb.WriteString("Rules:\n")
|
||||
sb.WriteString("- Always specify `assignee` when creating tasks\n")
|
||||
sb.WriteString("- Create all tasks first, then briefly tell the user what you delegated\n")
|
||||
sb.WriteString("- Do NOT say \"done\", \"xong\", \"finished\", or any completion language after delegating — delegation is NOT completion\n")
|
||||
sb.WriteString("- Do NOT phrase delegation as a question or request (\"nha?\", \"nhé?\", \"okay?\"). State it declaratively\n")
|
||||
sb.WriteString("- Results arrive automatically — do NOT present partial results\n")
|
||||
}
|
||||
sb.WriteString("\n## Follow-up Reminders\n\n")
|
||||
sb.WriteString("When you need user input/decision: create+claim task, then `ask_user` with text=<question>. ONLY use when you have a question for the user — NOT for waiting on teammates or status updates.\n")
|
||||
sb.WriteString("IMPORTANT: Present the question directly to the user in your response. `ask_user` only sets up periodic REMINDERS in case they don't reply — it does NOT present the question for you.\n")
|
||||
sb.WriteString("System auto-sends reminders. Call `clear_ask_user` when user replies.\n")
|
||||
|
||||
sb.WriteString("\nFor simple questions about team composition, answer directly from the member list above.\n")
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -143,8 +144,45 @@ func (t *TeamTasksTool) toDetailItem(ctx context.Context, task *store.TeamTaskDa
|
||||
}
|
||||
}
|
||||
|
||||
// buildCreateHint generates task creation guidance with member+model info.
|
||||
// Injected into search/list results so weaker models (MiniMax, Qwen) get
|
||||
// actionable hints before calling create.
|
||||
func (t *TeamTasksTool) buildCreateHint(ctx context.Context, teamID, leadAgentID, callerAgentID uuid.UUID) string {
|
||||
members, err := t.manager.cachedListMembers(ctx, teamID, callerAgentID)
|
||||
if err != nil || len(members) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[Task creation guide]\nAvailable members and their models:\n")
|
||||
for _, m := range members {
|
||||
if m.AgentID == leadAgentID {
|
||||
continue // skip lead from member list
|
||||
}
|
||||
model := ""
|
||||
if ag, err := t.manager.cachedGetAgentByID(ctx, m.AgentID); err == nil {
|
||||
model = ag.Model
|
||||
}
|
||||
entry := fmt.Sprintf("- %s (%s)", m.AgentKey, model)
|
||||
if m.Frontmatter != "" {
|
||||
fm := m.Frontmatter
|
||||
if len([]rune(fm)) > 80 {
|
||||
fm = string([]rune(fm)[:80]) + "…"
|
||||
}
|
||||
entry += " — " + fm
|
||||
}
|
||||
sb.WriteString(entry + "\n")
|
||||
}
|
||||
sb.WriteString("\nBefore creating a task:\n")
|
||||
sb.WriteString("1. DESCRIPTION: Include clear objective, relevant context, constraints, and expected output format. The member will only see this description — make it self-contained.\n")
|
||||
sb.WriteString("2. COMPLEXITY: If a task requires multiple steps or different skills, break it into separate tasks with blocked_by.\n")
|
||||
sb.WriteString("3. MODEL MATCH: Assign complex reasoning/analysis tasks to members with stronger models. Simple tasks can go to any.\n")
|
||||
sb.WriteString("4. INDEPENDENCE: Each task must be completable without asking the lead for clarification.")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]any) *Result {
|
||||
team, _, err := t.manager.resolveTeam(ctx)
|
||||
team, agentID, err := t.manager.resolveTeam(ctx)
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -208,6 +246,9 @@ func (t *TeamTasksTool) executeList(ctx context.Context, args map[string]any) *R
|
||||
if hasMore {
|
||||
resp["has_more"] = true
|
||||
}
|
||||
if hint := t.buildCreateHint(ctx, team.ID, team.LeadAgentID, agentID); hint != "" {
|
||||
resp["hint"] = hint
|
||||
}
|
||||
|
||||
out, _ := json.Marshal(resp)
|
||||
return SilentResult(string(out))
|
||||
@@ -322,7 +363,7 @@ func (t *TeamTasksTool) executeGet(ctx context.Context, args map[string]any) *Re
|
||||
}
|
||||
|
||||
func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]any) *Result {
|
||||
team, _, err := t.manager.resolveTeam(ctx)
|
||||
team, agentID, err := t.manager.resolveTeam(ctx)
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
@@ -358,9 +399,14 @@ func (t *TeamTasksTool) executeSearch(ctx context.Context, args map[string]any)
|
||||
items = append(items, t.toListItem(ctx, task))
|
||||
}
|
||||
|
||||
out, _ := json.Marshal(map[string]any{
|
||||
resp := map[string]any{
|
||||
"tasks": items,
|
||||
"count": len(items),
|
||||
})
|
||||
}
|
||||
if hint := t.buildCreateHint(ctx, team.ID, team.LeadAgentID, agentID); hint != "" {
|
||||
resp["hint"] = hint
|
||||
}
|
||||
|
||||
out, _ := json.Marshal(resp)
|
||||
return SilentResult(string(out))
|
||||
}
|
||||
@@ -94,27 +94,9 @@ func (t *TeamTasksTool) Parameters() map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// v2Actions lists team_tasks actions that require team version >= 2.
|
||||
var v2Actions = map[string]bool{
|
||||
"approve": true, "reject": true, "review": true, "comment": true,
|
||||
"progress": true, "attach": true, "update": true,
|
||||
"ask_user": true, "clear_ask_user": true, "retry": true,
|
||||
}
|
||||
|
||||
func (t *TeamTasksTool) Execute(ctx context.Context, args map[string]any) *Result {
|
||||
action, _ := args["action"].(string)
|
||||
|
||||
// Gate v2-only actions: resolve team once and check version.
|
||||
if v2Actions[action] {
|
||||
team, _, err := t.manager.resolveTeam(ctx)
|
||||
if err != nil {
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
if !IsTeamV2(team) {
|
||||
return ErrorResult(fmt.Sprintf("action '%s' requires team version 2 — upgrade in team settings", action))
|
||||
}
|
||||
}
|
||||
|
||||
switch action {
|
||||
case "list":
|
||||
return t.executeList(ctx, args)
|
||||
|
||||
@@ -80,6 +80,13 @@ func (m *TeamToolManager) dispatchTaskToAgent(ctx context.Context, task *store.T
|
||||
}
|
||||
}
|
||||
|
||||
// Hint: guide member on available team_tasks actions.
|
||||
content += "\n\n[Instructions]\n" +
|
||||
"- Use team_tasks(action=\"progress\", percent=N, text=\"...\") to report progress\n" +
|
||||
"- Use team_tasks(action=\"comment\", text=\"...\") to share findings or ask questions — comments are included in the task report sent to the leader\n" +
|
||||
"- When done: team_tasks(action=\"complete\", result=\"summary of your work\")\n" +
|
||||
"- Write output files to team workspace so lead can review"
|
||||
|
||||
// Use task's stored channel/chat as primary source for routing.
|
||||
// Falls back to ctx values for initial dispatch (task just created, fields match ctx).
|
||||
originChannel := task.Channel
|
||||
|
||||
@@ -53,20 +53,6 @@ func (m *TeamToolManager) agentDisplayName(ctx context.Context, key string) stri
|
||||
// Version helpers
|
||||
// ============================================================
|
||||
|
||||
// IsTeamV2 checks if team has version >= 2 in settings.
|
||||
// Returns false for nil team, nil/empty settings, or version < 2.
|
||||
func IsTeamV2(team *store.TeamData) bool {
|
||||
if team == nil || team.Settings == nil {
|
||||
return false
|
||||
}
|
||||
var s struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
if json.Unmarshal(team.Settings, &s) != nil {
|
||||
return false
|
||||
}
|
||||
return s.Version >= 2
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Follow-up settings helpers
|
||||
@@ -78,12 +64,8 @@ const (
|
||||
)
|
||||
|
||||
// followupDelayMinutes returns the team's followup_interval_minutes setting, or the default.
|
||||
// Returns 0 for v1 teams (followup disabled).
|
||||
func (m *TeamToolManager) followupDelayMinutes(team *store.TeamData) int {
|
||||
if !IsTeamV2(team) {
|
||||
return 0
|
||||
}
|
||||
if team.Settings == nil {
|
||||
if team == nil || team.Settings == nil {
|
||||
return defaultFollowupDelayMinutes
|
||||
}
|
||||
var settings map[string]any
|
||||
@@ -97,12 +79,8 @@ func (m *TeamToolManager) followupDelayMinutes(team *store.TeamData) int {
|
||||
}
|
||||
|
||||
// followupMaxReminders returns the team's followup_max_reminders setting, or the default.
|
||||
// Returns 0 for v1 teams (followup disabled).
|
||||
func (m *TeamToolManager) followupMaxReminders(team *store.TeamData) int {
|
||||
if !IsTeamV2(team) {
|
||||
return 0
|
||||
}
|
||||
if team.Settings == nil {
|
||||
if team == nil || team.Settings == nil {
|
||||
return defaultFollowupMaxReminders
|
||||
}
|
||||
var settings map[string]any
|
||||
@@ -130,12 +108,8 @@ const (
|
||||
)
|
||||
|
||||
// checkEscalation parses the team's escalation_mode and escalation_actions settings.
|
||||
// Returns EscalationNone for v1 teams.
|
||||
func (m *TeamToolManager) checkEscalation(team *store.TeamData, action string) EscalationResult {
|
||||
if !IsTeamV2(team) {
|
||||
return EscalationNone
|
||||
}
|
||||
if team.Settings == nil {
|
||||
if team == nil || team.Settings == nil {
|
||||
return EscalationNone
|
||||
}
|
||||
var settings map[string]any
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"versionAdvancedDesc": "All V1 features plus execution locking, follow-up reminders, review workflow, progress tracking, comments, audit trail, and auto-recovery.",
|
||||
"whatsNew": "What's different?",
|
||||
"versionModal": {
|
||||
"title": "Team Versions",
|
||||
"title": "Super Team Features",
|
||||
"feature": "Feature",
|
||||
"taskManagement": "Task Management",
|
||||
"taskManagementDesc": "Create, assign, complete, cancel tasks",
|
||||
@@ -263,7 +263,7 @@
|
||||
"escalationPolicyDesc": "Control what agents can do autonomously",
|
||||
"comingSoon": "Coming soon",
|
||||
"downgradeNote": "Downgrade is safe — V2 data is preserved but hidden.",
|
||||
"betaNote": "V2 is currently in Beta.",
|
||||
"betaNote": "Some features are currently in Beta.",
|
||||
"gotIt": "Got it"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"versionAdvancedDesc": "Tất cả tính năng V1 cùng khóa thực thi, nhắc nhở theo dõi, quy trình duyệt, theo dõi tiến độ, bình luận, nhật ký hoạt động và tự phục hồi.",
|
||||
"whatsNew": "Có gì mới?",
|
||||
"versionModal": {
|
||||
"title": "Phiên bản Team",
|
||||
"title": "Tính năng Super Team",
|
||||
"feature": "Tính năng",
|
||||
"taskManagement": "Quản lý nhiệm vụ",
|
||||
"taskManagementDesc": "Tạo, phân công, hoàn thành, hủy nhiệm vụ",
|
||||
@@ -263,7 +263,7 @@
|
||||
"escalationPolicyDesc": "Kiểm soát những gì agent có thể tự thực hiện",
|
||||
"comingSoon": "Sắp ra mắt",
|
||||
"downgradeNote": "Hạ cấp an toàn — dữ liệu V2 được giữ lại nhưng ẩn đi.",
|
||||
"betaNote": "V2 hiện đang ở giai đoạn Beta.",
|
||||
"betaNote": "Một số tính năng đang ở giai đoạn Beta.",
|
||||
"gotIt": "Đã hiểu"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -241,7 +241,7 @@
|
||||
"versionAdvancedDesc": "所有V1功能,外加执行锁定、跟进提醒、审核工作流、进度追踪、评论、审计日志和自动恢复。",
|
||||
"whatsNew": "有什么不同?",
|
||||
"versionModal": {
|
||||
"title": "团队版本",
|
||||
"title": "超级团队功能",
|
||||
"feature": "功能",
|
||||
"taskManagement": "任务管理",
|
||||
"taskManagementDesc": "创建、分配、完成、取消任务",
|
||||
@@ -263,7 +263,7 @@
|
||||
"escalationPolicyDesc": "控制代理可以自主执行的操作",
|
||||
"comingSoon": "即将推出",
|
||||
"downgradeNote": "降级是安全的 — V2数据会保留但隐藏。",
|
||||
"betaNote": "V2目前处于Beta阶段。",
|
||||
"betaNote": "部分功能目前处于Beta阶段。",
|
||||
"gotIt": "知道了"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ArrowLeft, Settings, Trash2, Users } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TeamData, TeamMemberData, TeamAccessSettings } from "@/types/team";
|
||||
import type { TeamData, TeamMemberData } from "@/types/team";
|
||||
import { TeamFeaturesModal } from "../team-features-modal";
|
||||
|
||||
interface BoardHeaderProps {
|
||||
team: TeamData;
|
||||
@@ -11,13 +13,11 @@ interface BoardHeaderProps {
|
||||
onDelete: () => void;
|
||||
onSettings: () => void;
|
||||
onMembers: () => void;
|
||||
onV2Click?: () => void;
|
||||
}
|
||||
|
||||
export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMembers, onV2Click }: BoardHeaderProps) {
|
||||
export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMembers }: BoardHeaderProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const settings = (team.settings ?? {}) as TeamAccessSettings;
|
||||
const isV2 = (settings.version ?? 1) >= 2;
|
||||
const [featuresOpen, setFeaturesOpen] = useState(false);
|
||||
const leadMember = members.find((m) => m.role === "lead");
|
||||
const leadName = leadMember?.display_name || leadMember?.agent_key
|
||||
|| team.lead_display_name || team.lead_agent_key;
|
||||
@@ -36,14 +36,11 @@ export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMem
|
||||
<Badge variant={team.status === "active" ? "success" : "secondary"} className="text-[10px]">
|
||||
{team.status}
|
||||
</Badge>
|
||||
{isV2 && (
|
||||
<Badge
|
||||
className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold shadow-sm cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={onV2Click}
|
||||
>
|
||||
v2 Super Team (Beta)
|
||||
<button type="button" className="relative inline-flex items-center" onClick={() => setFeaturesOpen(true)}>
|
||||
<Badge className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold hover:from-orange-600 hover:to-amber-600">
|
||||
v2 Super Team
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{leadName && (
|
||||
@@ -75,6 +72,8 @@ export function BoardHeader({ team, members, onBack, onDelete, onSettings, onMem
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">{t("delete.title")}</span>
|
||||
</Button>
|
||||
|
||||
<TeamFeaturesModal open={featuresOpen} onOpenChange={setFeaturesOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TeamSettingsTab } from "../team-settings-tab";
|
||||
import { TeamVersionModal } from "../team-version-modal";
|
||||
import type { TeamData, TeamMemberData, TeamAccessSettings } from "@/types/team";
|
||||
import { TeamFeaturesModal } from "../team-features-modal";
|
||||
import type { TeamData, TeamMemberData } from "@/types/team";
|
||||
|
||||
interface TeamInfoDialogProps {
|
||||
open: boolean;
|
||||
@@ -21,10 +21,7 @@ export function TeamInfoDialog({
|
||||
open, onOpenChange, team, teamId, members, onSaved,
|
||||
}: TeamInfoDialogProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
const [versionModalOpen, setVersionModalOpen] = useState(false);
|
||||
|
||||
const settings = (team.settings ?? {}) as TeamAccessSettings;
|
||||
const isV2 = (settings.version ?? 1) >= 2;
|
||||
const [featuresOpen, setFeaturesOpen] = useState(false);
|
||||
|
||||
// Resolve lead name from members (more reliable than team.lead_display_name which can be empty)
|
||||
const leadMember = members.find((m) => m.role === "lead");
|
||||
@@ -41,14 +38,11 @@ export function TeamInfoDialog({
|
||||
<Badge variant={team.status === "active" ? "success" : "secondary"} className="text-[10px]">
|
||||
{team.status}
|
||||
</Badge>
|
||||
{isV2 && (
|
||||
<Badge
|
||||
className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold shadow-sm cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => setVersionModalOpen(true)}
|
||||
>
|
||||
v2 Super Team (Beta)
|
||||
<button type="button" className="relative inline-flex items-center" onClick={() => setFeaturesOpen(true)}>
|
||||
<Badge className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold hover:from-orange-600 hover:to-amber-600">
|
||||
v2 Super Team
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -76,8 +70,7 @@ export function TeamInfoDialog({
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<TeamVersionModal open={versionModalOpen} onOpenChange={setVersionModalOpen} />
|
||||
<TeamFeaturesModal open={featuresOpen} onOpenChange={setFeaturesOpen} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { BoardContainer } from "./board/board-container";
|
||||
import { TeamInfoDialog } from "./board/team-info-dialog";
|
||||
import { TeamMembersDialog } from "./board/team-members-dialog";
|
||||
import { TeamWorkspaceDialog } from "./board/team-workspace-dialog";
|
||||
import { TeamVersionModal } from "./team-version-modal";
|
||||
import type { TeamData, TeamMemberData, TeamAccessSettings, ScopeEntry } from "@/types/team";
|
||||
|
||||
interface TeamDetailPageProps {
|
||||
@@ -41,7 +40,6 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const [membersOpen, setMembersOpen] = useState(false);
|
||||
const [workspaceOpen, setWorkspaceOpen] = useState(false);
|
||||
const [versionModalOpen, setVersionModalOpen] = useState(false);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
@@ -97,7 +95,6 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
onDelete={() => setDeleteOpen(true)}
|
||||
onSettings={() => setInfoOpen(true)}
|
||||
onMembers={() => setMembersOpen(true)}
|
||||
onV2Click={() => setVersionModalOpen(true)}
|
||||
/>
|
||||
|
||||
<BoardContainer
|
||||
@@ -141,9 +138,6 @@ export function TeamDetailPage({ teamId, onBack }: TeamDetailPageProps) {
|
||||
scopes={scopes}
|
||||
/>
|
||||
|
||||
{/* V2 version comparison modal */}
|
||||
<TeamVersionModal open={versionModalOpen} onOpenChange={setVersionModalOpen} />
|
||||
|
||||
{/* Delete confirmation */}
|
||||
<ConfirmDeleteDialog
|
||||
open={deleteOpen}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Check, Minus,
|
||||
ListTodo, FolderOpen, Lock, Bell,
|
||||
ClipboardCheck, BarChart3, MessageSquare, RotateCcw, ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TeamFeaturesModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ key: "taskManagement", icon: ListTodo },
|
||||
{ key: "sharedWorkspace", icon: FolderOpen },
|
||||
{ key: "executionLocking", icon: Lock },
|
||||
{ key: "followupReminders", icon: Bell },
|
||||
{ key: "progressTracking", icon: BarChart3 },
|
||||
{ key: "autoRecovery", icon: RotateCcw },
|
||||
{ key: "commentsAudit", icon: MessageSquare, comingSoon: true },
|
||||
{ key: "escalationPolicy", icon: ShieldCheck, comingSoon: true },
|
||||
{ key: "reviewWorkflow", icon: ClipboardCheck, comingSoon: true },
|
||||
] as const;
|
||||
|
||||
export function TeamFeaturesModal({ open, onOpenChange }: TeamFeaturesModalProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[85vh] w-[95vw] flex flex-col sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{t("settings.versionModal.title")}
|
||||
<Badge className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold">
|
||||
Beta
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t("settings.versionModal.title")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto min-h-0 space-y-4 -mx-4 px-4 sm:-mx-6 sm:px-6">
|
||||
{/* Feature list */}
|
||||
<div className="space-y-1">
|
||||
{FEATURES.map((f) => {
|
||||
const Icon = f.icon;
|
||||
const dimmed = "comingSoon" in f && f.comingSoon;
|
||||
return (
|
||||
<div
|
||||
key={f.key}
|
||||
className={`group grid grid-cols-[1fr_40px] items-center gap-2 rounded-lg px-3 py-2.5 hover:bg-muted/30 ${dimmed ? "opacity-40" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<Icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium flex items-center gap-2">
|
||||
{t(`settings.versionModal.${f.key}`)}
|
||||
{dimmed && (
|
||||
<Badge variant="outline" className="text-[9px] px-1.5 py-0 font-normal">
|
||||
{t("settings.versionModal.comingSoon")}
|
||||
</Badge>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{t(`settings.versionModal.${f.key}Desc`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{dimmed ? (
|
||||
<Minus className="h-4 w-4 text-muted-foreground/30" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-1 rounded-lg border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
|
||||
<p>{t("settings.versionModal.betaNote")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>
|
||||
{t("settings.versionModal.gotIt")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Combobox } from "@/components/ui/combobox";
|
||||
import { X, Save, Bell, ShieldAlert, Clock, Info, FolderLock, FolderSync, Zap, Bot, Loader2 } from "lucide-react";
|
||||
import { X, Save, Bell, ShieldAlert, Clock, FolderLock, FolderSync, Zap, Bot, Loader2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CHANNEL_TYPES } from "@/constants/channels";
|
||||
import type { TeamData, TeamAccessSettings, TeamNotifyConfig, EscalationMode, EscalationAction } from "@/types/team";
|
||||
import { useTeams } from "./hooks/use-teams";
|
||||
import { TeamVersionModal } from "./team-version-modal";
|
||||
|
||||
interface TeamSettingsTabProps {
|
||||
teamId: string;
|
||||
@@ -66,7 +65,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
|
||||
// Parse initial settings
|
||||
const initial = (team.settings ?? {}) as TeamAccessSettings;
|
||||
const [version, setVersion] = useState<number>(initial.version ?? 1);
|
||||
const [allowUserIds, setAllowUserIds] = useState<string[]>(initial.allow_user_ids ?? []);
|
||||
const [denyUserIds, setDenyUserIds] = useState<string[]>(initial.deny_user_ids ?? []);
|
||||
const [allowChannels, setAllowChannels] = useState<string[]>(initial.allow_channels ?? []);
|
||||
@@ -88,10 +86,8 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
const [followupInterval, setFollowupInterval] = useState<number>(initial.followup_interval_minutes ?? 30);
|
||||
const [followupMaxReminders, setFollowupMaxReminders] = useState<number>(initial.followup_max_reminders ?? 0);
|
||||
const [workspaceScope, setWorkspaceScope] = useState<string>(initial.workspace_scope ?? "isolated");
|
||||
const isTeamV2 = version >= 2;
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [versionModalOpen, setVersionModalOpen] = useState(false);
|
||||
|
||||
// Load known users for combobox
|
||||
useEffect(() => {
|
||||
@@ -101,7 +97,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
// Reset when team changes
|
||||
useEffect(() => {
|
||||
const s = (team.settings ?? {}) as TeamAccessSettings;
|
||||
setVersion(s.version ?? 1);
|
||||
setAllowUserIds(s.allow_user_ids ?? []);
|
||||
setDenyUserIds(s.deny_user_ids ?? []);
|
||||
setAllowChannels(s.allow_channels ?? []);
|
||||
@@ -157,70 +152,19 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
if (followupInterval !== 30) settings.followup_interval_minutes = followupInterval;
|
||||
if (followupMaxReminders !== 0) settings.followup_max_reminders = followupMaxReminders;
|
||||
settings.workspace_scope = workspaceScope || "isolated";
|
||||
if (version >= 2) settings.version = version;
|
||||
await updateTeamSettings(teamId, settings);
|
||||
onSaved();
|
||||
} catch { // toast shown by hook
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [teamId, version, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifyCompleted, notifyCommented, notifyNewTask, notifySlowTool, notifyMode, memberRequestsEnabled, memberRequestsAutoDispatch, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved, t]);
|
||||
}, [teamId, allowUserIds, denyUserIds, allowChannels, denyChannels, notifyDispatched, notifyProgress, notifyFailed, notifyCompleted, notifyCommented, notifyNewTask, notifySlowTool, notifyMode, memberRequestsEnabled, memberRequestsAutoDispatch, escalationMode, escalationActions, followupInterval, followupMaxReminders, workspaceScope, updateTeamSettings, onSaved]);
|
||||
|
||||
const userOptions = knownUsers.map((u) => ({ value: u, label: u }));
|
||||
const channelOptions = CHANNEL_TYPES.map((c) => ({ value: c.value, label: c.label }));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Team Version */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">{t("settings.teamVersion")}</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVersionModalOpen(true)}
|
||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
{t("settings.whatsNew")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVersion(1)}
|
||||
className={
|
||||
"rounded-lg border p-3 text-left transition-colors " +
|
||||
(version === 1
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50")
|
||||
}
|
||||
>
|
||||
<div className="text-sm font-medium">V1 — {t("settings.versionBasic")}</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t("settings.versionBasicDesc")}
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVersion(2)}
|
||||
className={
|
||||
"rounded-lg border p-3 text-left transition-colors " +
|
||||
(version >= 2
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">V2 — {t("settings.versionAdvanced")}</span>
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">Beta</Badge>
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
{t("settings.versionAdvancedDesc")}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">{t("settings.notifications")}</h3>
|
||||
@@ -384,8 +328,8 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Escalation Policy (V2 only — coming soon) */}
|
||||
{isTeamV2 && <div className="space-y-4 opacity-40 pointer-events-none select-none">
|
||||
{/* Escalation Policy (coming soon) */}
|
||||
<div className="space-y-4 opacity-40 pointer-events-none select-none">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
{t("settings.escalationPolicy")}
|
||||
<span className="rounded border px-1.5 py-0.5 text-[9px] font-normal text-muted-foreground">{t("settings.versionModal.comingSoon")}</span>
|
||||
@@ -403,10 +347,10 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* Follow-up Reminders (V2 only) */}
|
||||
{isTeamV2 && <div className="space-y-4">
|
||||
{/* Follow-up Reminders */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium">{t("settings.followupReminders")}</h3>
|
||||
<div className="rounded-lg border bg-gradient-to-r from-amber-500/5 to-yellow-500/5 p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
@@ -445,7 +389,7 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* User Access Control */}
|
||||
<div className="space-y-4">
|
||||
@@ -517,7 +461,6 @@ export function TeamSettingsTab({ teamId, team, onSaved }: TeamSettingsTabProps)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<TeamVersionModal open={versionModalOpen} onOpenChange={setVersionModalOpen} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Check, Minus,
|
||||
ListTodo, FolderOpen, Lock, Bell,
|
||||
ClipboardCheck, BarChart3, MessageSquare, RotateCcw, ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TeamVersionModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ key: "taskManagement", icon: ListTodo, v1: true },
|
||||
{ key: "sharedWorkspace", icon: FolderOpen, v1: true },
|
||||
{ key: "executionLocking", icon: Lock, v1: false },
|
||||
{ key: "followupReminders", icon: Bell, v1: false },
|
||||
{ key: "progressTracking", icon: BarChart3, v1: false },
|
||||
{ key: "autoRecovery", icon: RotateCcw, v1: false },
|
||||
{ key: "commentsAudit", icon: MessageSquare, v1: false, comingSoon: true },
|
||||
{ key: "escalationPolicy", icon: ShieldCheck, v1: false, comingSoon: true },
|
||||
{ key: "reviewWorkflow", icon: ClipboardCheck, v1: false, comingSoon: true },
|
||||
] as const;
|
||||
|
||||
export function TeamVersionModal({ open, onOpenChange }: TeamVersionModalProps) {
|
||||
const { t } = useTranslation("teams");
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-h-[85vh] w-[95vw] flex flex-col sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{t("settings.versionModal.title")}
|
||||
<Badge className="bg-gradient-to-r from-orange-500 to-amber-500 text-[10px] px-2 py-0.5 text-white border-0 font-semibold">
|
||||
Beta
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">{t("settings.versionModal.title")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-y-auto min-h-0 space-y-4 -mx-4 px-4 sm:-mx-6 sm:px-6">
|
||||
{/* Version columns header */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-center">
|
||||
<p className="text-lg font-bold">V1</p>
|
||||
<p className="text-xs text-muted-foreground">{t("settings.versionBasicDesc")}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-orange-500/30 bg-gradient-to-br from-orange-500/5 to-amber-500/5 p-3 text-center">
|
||||
<p className="text-lg font-bold bg-gradient-to-r from-orange-600 to-amber-600 bg-clip-text text-transparent">V2 Super Team</p>
|
||||
<p className="text-xs text-muted-foreground">{t("settings.versionAdvancedDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature list */}
|
||||
<div className="space-y-1">
|
||||
{FEATURES.map((f) => {
|
||||
const Icon = f.icon;
|
||||
const dimmed = "comingSoon" in f && f.comingSoon;
|
||||
return (
|
||||
<div
|
||||
key={f.key}
|
||||
className={`group grid grid-cols-[1fr_60px_60px] items-center gap-2 rounded-lg px-3 py-2.5 hover:bg-muted/30 ${dimmed ? "opacity-40" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
<Icon className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium flex items-center gap-2">
|
||||
{t(`settings.versionModal.${f.key}`)}
|
||||
{dimmed && (
|
||||
<Badge variant="outline" className="text-[9px] px-1.5 py-0 font-normal">
|
||||
{t("settings.versionModal.comingSoon")}
|
||||
</Badge>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
{t(`settings.versionModal.${f.key}Desc`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{f.v1 ? (
|
||||
<Check className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
) : (
|
||||
<Minus className="h-4 w-4 text-muted-foreground/30" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
{dimmed ? (
|
||||
<Minus className="h-4 w-4 text-muted-foreground/30" />
|
||||
) : (
|
||||
<Check className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Column labels for V1/V2 */}
|
||||
<div className="grid grid-cols-[1fr_60px_60px] gap-2 px-3 text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
<span />
|
||||
<span className="text-center">V1</span>
|
||||
<span className="text-center">V2</span>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-1 rounded-lg border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
|
||||
<p>{t("settings.versionModal.downgradeNote")}</p>
|
||||
<p>{t("settings.versionModal.betaNote")}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>
|
||||
{t("settings.versionModal.gotIt")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user