0306
This commit is contained in:
parent
b37e66b9a5
commit
122ab6ef3e
@ -9,7 +9,10 @@
|
||||
"Bash(npm install:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(curl:*)"
|
||||
"Bash(curl:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(echo DEEPSEEK_API_KEY=$DEEPSEEK_API_KEY:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,38 @@
|
||||
---
|
||||
name: 法律咨询团队
|
||||
description: 法律咨询团队,包含合同审查、法律风险评估和合规管理
|
||||
description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
|
||||
author: Agent Team
|
||||
version: 1.0.0
|
||||
repo_url: https://gitea.catter.cn/agent-teams/legal-team.git
|
||||
agents:
|
||||
- 法律总监
|
||||
- 合同律师
|
||||
- 合规专员
|
||||
skills:
|
||||
- 合同审查
|
||||
- 法律知识库
|
||||
---
|
||||
|
||||
# 法律咨询团队
|
||||
|
||||
专业法律咨询团队,提供全方位的法律服务支持。
|
||||
|
||||
## 团队成员
|
||||
|
||||
- **法律总监**:团队负责人,协调各项法律事务,提供战略级法律建议
|
||||
- **合同律师**:专注合同审查、起草和风险评估
|
||||
- **合规专员**:负责合规检查、风险识别和合规建议
|
||||
|
||||
## 核心能力
|
||||
|
||||
- 合同审查与风险评估
|
||||
- 法律咨询与建议
|
||||
- 合规检查与指导
|
||||
- 法律文档起草
|
||||
|
||||
## 使用场景
|
||||
|
||||
- 企业合同审查
|
||||
- 法律风险评估
|
||||
- 合规体系建设
|
||||
- 法律咨询问答
|
||||
16
agents/legal-team/法律总监/memory/2026-03.md
Normal file
16
agents/legal-team/法律总监/memory/2026-03.md
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
## 2026-03-05 — ä½ å¥½
|
||||
|
||||
ASSIGN:å<>ˆå<CB86>Œå¾‹å¸ˆ:分æž<C3A6>用户"第五å£"çš„å·¥ä½œé£Žæ ¼å’Œæ²Ÿé€šå<C5A1><C3A5>好,整ç<C2B4>†æˆ<C3A6>简æ´<C3A6>è¦<C3A8>点,为å<C2BA>Žç»æ³•律咨询æ<C2A2><C3A6>ä¾›å<E280BA>‚考框架。
|
||||
|
||||
ASSIGN:å<>ˆè§„专员:评估用户简介ä¸å<C2AD>¯èƒ½æ¶‰å<E280B0>Šçš„æ³•律å<E280B9>ˆè§„风险点(如沟通方å¼<C3A5>ã€<C3A3>æ•°æ<C2B0>®ä½¿ç”¨ç‰ï¼‰ï¼Œæ<C592><C3A6>出åˆ<C3A5>æ¥æ³¨æ„<C3A6>事项。
|
||||
|
||||
## 2026-03-05 — 我一个ä¸é—´äººï¼Œè¯´å<C2B4>¯ä»¥ç»™æˆ‘介ç»<C3A7>一个è¶
|
||||
|
||||
基于本次咨询,总结关键è¦<EFBFBD>点如下:
|
||||
|
||||
- **æ<>ƒå±žæ ¸æŸ¥æ˜¯åŸºçŸ³**:投资任何实体项目å‰<C3A5>,**å¿…é¡»**é¦–å…ˆå®¡æŸ¥å¹¶ç¡®è®¤æ ¸å¿ƒèµ„äº§ï¼ˆå¦‚åœŸåœ°ã€<C3A3>场地)的完整ã€<C3A3>å<EFBFBD>ˆæ³•æ<E280A2>ƒå±žé“¾æ<C2BE>¡å<C2A1>Šä½¿ç”¨è®¸å<C2B8>¯ï¼Œæ<C592>ƒå±žä¸<C3A4>æ¸…æ˜¯æ ¹æœ¬æ€§é£Žé™©ã€‚
|
||||
- **ä¸é—´äººæ¨¡å¼<C3A5>风险高**ï¼šé€šè¿‡æ— æ˜Žç¡®æŽˆæ<CB86>ƒçš„“ä¸é—´äººâ€<C3A2>进行投资,会导致法律关系模糊ã€<C3A3>资金失控,且æž<C3A6>易与项目实际方脱节,应争å<E280B0>–与æ<C5BD>ƒå±žæ–¹å»ºç«‹ç›´æŽ¥å<C2A5>ˆå<CB86>Œå…³ç³»ã€‚
|
||||
- **异常高回报是å<C2AF>±é™©ä¿¡å<C2A1>·**:远高于市场平å<C2B3>‡æ°´å¹³çš„回报承诺(如年化30%)通常伴éš<C3A9>æž<C3A6>高的è¿<C3A8>约风险或项目本身ä¸<C3A4>å<EFBFBD>¯è¡Œï¼Œéœ€æ·±ç©¶å…¶å•†ä¸šå<C5A1>ˆç<CB86>†æ€§ä¸Žæ³•律å<E280B9>ˆè§„性。
|
||||
- **å<><C3A5>议性质必须明确**:“投资å<E2809E><C3A5>è®®â€<C3A2>的法律定性(借贷ã€<C3A3>å<EFBFBD>ˆä¼™æˆ–委托)直接决定您的æ<E2809E>ƒåˆ©ã€<C3A3>风险与责任,必须在æ<C2A8>¡æ¬¾ä¸æ¸…晰界定,é<C592>¿å…<C3A5>å<EFBFBD>Žç»äº‰è®®ã€‚
|
||||
- **å<>ˆè§„红线ä¸<C3A4>å<EFBFBD>¯è§¦ç¢°**:涉å<E280B0>Šæ”¿åºœèµ„æº<C3A6>或资产的项目,若è¿<C3A8>作模å¼<C3A5>ä¾<C3A4>赖“关系â€<C3A2>而é<C592>žå…¬å¼€ç¨‹åº<C3A5>,æž<C3A6>易涉å<E280B0>Šå•†ä¸šè´¿èµ‚ã€<C3A3>è¿<C3A8>è§„èž<C3A8>资ç‰åˆ‘事风险,必须å<C2BB>šæŒ<C3A6>å<EFBFBD>ˆæ³•é€<C3A9>明路径。
|
||||
@ -62,6 +62,7 @@ func (s *Server) routes() {
|
||||
g := s.e.Group("/api")
|
||||
g.GET("/rooms", s.listRooms)
|
||||
g.POST("/rooms", s.createRoom)
|
||||
g.PUT("/rooms/:id/team", s.setRoomTeam)
|
||||
g.GET("/agents", s.listAgents)
|
||||
g.GET("/agents/:name/files/:file", s.readAgentFile)
|
||||
g.PUT("/agents/:name/files/:file", s.writeAgentFile)
|
||||
@ -191,15 +192,18 @@ func (s *Server) listRooms(c echo.Context) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
type roomInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status room.Status `json:"status"`
|
||||
Master string `json:"master"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status room.Status `json:"status"`
|
||||
Master string `json:"master"`
|
||||
Members []string `json:"members"`
|
||||
Color string `json:"color"`
|
||||
Team string `json:"team"`
|
||||
}
|
||||
var list []roomInfo
|
||||
for id, r := range s.rooms {
|
||||
list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master})
|
||||
list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master, Members: r.Config.Members, Color: r.Config.Color, Team: r.Config.Team})
|
||||
}
|
||||
return c.JSON(200, list)
|
||||
}
|
||||
@ -209,16 +213,14 @@ func (s *Server) createRoom(c echo.Context) error {
|
||||
if err := c.Bind(&cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Type == "" {
|
||||
cfg.Type = room.TypeDept
|
||||
}
|
||||
dir := filepath.Join(s.roomsDir, cfg.Name)
|
||||
os.MkdirAll(filepath.Join(dir, "workspace"), 0755)
|
||||
os.MkdirAll(filepath.Join(dir, "history"), 0755)
|
||||
|
||||
content := "---\nname: " + cfg.Name + "\ntype: " + string(cfg.Type) + "\nmaster: " + cfg.Master + "\nmembers:\n"
|
||||
for _, m := range cfg.Members {
|
||||
content += " - " + m + "\n"
|
||||
}
|
||||
content += "---\n"
|
||||
os.WriteFile(filepath.Join(dir, "room.md"), []byte(content), 0644)
|
||||
s.writeRoomFile(dir, cfg)
|
||||
|
||||
r, err := room.Load(dir, s.agentsDir, s.skillsDir)
|
||||
if err != nil {
|
||||
@ -231,6 +233,76 @@ func (s *Server) createRoom(c echo.Context) error {
|
||||
return c.JSON(201, map[string]string{"id": cfg.Name})
|
||||
}
|
||||
|
||||
func (s *Server) writeRoomFile(dir string, cfg room.Config) {
|
||||
content := "---\nname: " + cfg.Name + "\ntype: " + string(cfg.Type) + "\nmaster: " + cfg.Master + "\ncolor: " + cfg.Color + "\nteam: " + cfg.Team + "\nmembers:\n"
|
||||
for _, m := range cfg.Members {
|
||||
content += " - " + m + "\n"
|
||||
}
|
||||
content += "---\n"
|
||||
os.WriteFile(filepath.Join(dir, "room.md"), []byte(content), 0644)
|
||||
}
|
||||
|
||||
func (s *Server) setRoomTeam(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
var body struct {
|
||||
Team string `json:"team"`
|
||||
}
|
||||
if err := c.Bind(&body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load team agents
|
||||
teamDir := filepath.Join(s.teamsDir, body.Team)
|
||||
team, err := loadTeam(filepath.Join(teamDir, "team.yaml"))
|
||||
if err != nil {
|
||||
return c.JSON(404, map[string]string{"error": "team not found"})
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
r := s.rooms[id]
|
||||
if r == nil {
|
||||
return c.JSON(404, map[string]string{"error": "room not found"})
|
||||
}
|
||||
|
||||
// Find master and members from team agents by reading AGENT.md
|
||||
master := ""
|
||||
var members []string
|
||||
for _, agentName := range team.Agents {
|
||||
agentMD, err := os.ReadFile(filepath.Join(s.agentsDir, body.Team, agentName, "AGENT.md"))
|
||||
if err == nil && strings.Contains(string(agentMD), "role: master") {
|
||||
master = agentName
|
||||
} else {
|
||||
members = append(members, agentName)
|
||||
}
|
||||
}
|
||||
if master == "" && len(team.Agents) > 0 {
|
||||
master = team.Agents[0]
|
||||
members = team.Agents[1:]
|
||||
}
|
||||
|
||||
cfg := r.Config
|
||||
cfg.Team = body.Team
|
||||
cfg.Master = master
|
||||
cfg.Members = members
|
||||
|
||||
s.writeRoomFile(r.Dir, cfg)
|
||||
|
||||
newRoom, err := room.Load(r.Dir, s.agentsDir, s.skillsDir)
|
||||
if err != nil {
|
||||
return c.JSON(500, map[string]string{"error": err.Error()})
|
||||
}
|
||||
newRoom.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
|
||||
newRoom.User = s.user
|
||||
s.rooms[id] = newRoom
|
||||
|
||||
return c.JSON(200, map[string]interface{}{
|
||||
"team": body.Team,
|
||||
"master": master,
|
||||
"members": members,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) listAgents(c echo.Context) error {
|
||||
entries, _ := os.ReadDir(s.agentsDir)
|
||||
type agentInfo struct {
|
||||
@ -393,7 +465,6 @@ func (s *Server) getWorkspaceFile(c echo.Context) error {
|
||||
|
||||
func (s *Server) getMessages(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
// Read today's history file and parse into messages
|
||||
historyFile := filepath.Join(s.roomsDir, id, "history", time.Now().Format("2006-01-02")+".md")
|
||||
data, err := os.ReadFile(historyFile)
|
||||
if err != nil {
|
||||
@ -406,30 +477,40 @@ func (s *Server) getMessages(c echo.Context) error {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
var msgs []msg
|
||||
lines := strings.Split(string(data), "\n## ")
|
||||
for i, block := range lines {
|
||||
// Format: "\n**[HH:MM:SS] agentName** (role)\n\ncontent\n"
|
||||
blocks := strings.Split(string(data), "\n**[")
|
||||
for i, block := range blocks {
|
||||
if block == "" {
|
||||
continue
|
||||
}
|
||||
// Parse "**[HH:MM:SS] agentName** (role)\n\ncontent"
|
||||
parts := strings.SplitN(block, "\n\n", 2)
|
||||
if len(parts) < 2 {
|
||||
// block starts with "HH:MM:SS] agentName** (role)\n\ncontent"
|
||||
headerEnd := strings.Index(block, "\n\n")
|
||||
if headerEnd < 0 {
|
||||
continue
|
||||
}
|
||||
header := parts[0]
|
||||
content := strings.TrimSpace(parts[1])
|
||||
// Extract role and agent from header like "**[15:04:05] agentName** (role)"
|
||||
var agentName, role string
|
||||
fmt.Sscanf(header, "**[%*s] %s** (%s)", &agentName, &role)
|
||||
agentName = strings.TrimSuffix(agentName, "**")
|
||||
role = strings.TrimSuffix(role, ")")
|
||||
header := block[:headerEnd]
|
||||
content := strings.TrimSpace(block[headerEnd+2:])
|
||||
// header: "HH:MM:SS] agentName** (role)"
|
||||
bracketEnd := strings.Index(header, "] ")
|
||||
if bracketEnd < 0 {
|
||||
continue
|
||||
}
|
||||
rest := header[bracketEnd+2:] // "agentName** (role)"
|
||||
starIdx := strings.Index(rest, "**")
|
||||
if starIdx < 0 {
|
||||
continue
|
||||
}
|
||||
agentName := rest[:starIdx]
|
||||
roleStr := strings.TrimSpace(rest[starIdx+2:]) // " (role)"
|
||||
roleStr = strings.TrimPrefix(roleStr, "(")
|
||||
roleStr = strings.TrimSuffix(roleStr, ")")
|
||||
if agentName == "" {
|
||||
agentName = "unknown"
|
||||
}
|
||||
if role == "" {
|
||||
role = "member"
|
||||
if roleStr == "" {
|
||||
roleStr = "member"
|
||||
}
|
||||
msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: role, Content: content})
|
||||
msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: roleStr, Content: content})
|
||||
}
|
||||
return c.JSON(200, msgs)
|
||||
}
|
||||
@ -529,6 +610,7 @@ func (s *Server) listTeams(c echo.Context) error {
|
||||
continue
|
||||
}
|
||||
teams = append(teams, map[string]interface{}{
|
||||
"id": e.Name(),
|
||||
"name": team.Name,
|
||||
"description": team.Description,
|
||||
"author": team.Author,
|
||||
@ -650,22 +732,36 @@ func loadTeam(path string) (*hub.Team, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Simple YAML parse
|
||||
team := &hub.Team{}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
var currentField string
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "name: ") {
|
||||
team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: "))
|
||||
currentField = ""
|
||||
} else if strings.HasPrefix(line, "description: ") {
|
||||
team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: "))
|
||||
currentField = ""
|
||||
} else if strings.HasPrefix(line, "author: ") {
|
||||
team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: "))
|
||||
currentField = ""
|
||||
} else if strings.HasPrefix(line, "repo_url: ") {
|
||||
team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: "))
|
||||
currentField = ""
|
||||
} else if strings.HasPrefix(line, "installed_at: ") {
|
||||
team.InstalledAt = strings.TrimSpace(strings.TrimPrefix(line, "installed_at: "))
|
||||
currentField = ""
|
||||
} else if strings.HasPrefix(line, "agents:") {
|
||||
currentField = "agents"
|
||||
} else if strings.HasPrefix(line, "skills:") {
|
||||
currentField = "skills"
|
||||
} else if strings.HasPrefix(line, " - ") {
|
||||
team.Agents = append(team.Agents, strings.TrimSpace(strings.TrimPrefix(line, " - ")))
|
||||
item := strings.TrimSpace(strings.TrimPrefix(line, " - "))
|
||||
if currentField == "agents" {
|
||||
team.Agents = append(team.Agents, item)
|
||||
} else if currentField == "skills" {
|
||||
team.Skills = append(team.Skills, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return team, nil
|
||||
|
||||
@ -8,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
@ -108,10 +110,14 @@ func installFromDir(tmp, teamName, repoURL, agentsDir, skillsDir, teamsDir strin
|
||||
// Create knowledge dir
|
||||
os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755)
|
||||
|
||||
// Read TEAM.md for metadata
|
||||
teamMeta := readTeamMeta(tmp)
|
||||
|
||||
// Create team record
|
||||
team := &Team{
|
||||
Name: teamName,
|
||||
Description: "团队描述",
|
||||
Name: teamMeta.Name,
|
||||
Description: teamMeta.Description,
|
||||
Author: teamMeta.Author,
|
||||
RepoURL: repoURL,
|
||||
Agents: installedAgents,
|
||||
Skills: installedSkills,
|
||||
@ -238,3 +244,30 @@ func copyDir(src, dst string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readTeamMeta(tmp string) Team {
|
||||
team := Team{}
|
||||
teamFile := filepath.Join(tmp, "TEAM.md")
|
||||
data, err := os.ReadFile(teamFile)
|
||||
if err != nil {
|
||||
return Team{Name: extractRepoName(tmp)}
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if strings.HasPrefix(content, "---") {
|
||||
parts := strings.SplitN(content, "---", 3)
|
||||
if len(parts) >= 3 {
|
||||
yamlContent := parts[1]
|
||||
yaml.Unmarshal([]byte(yamlContent), &team)
|
||||
}
|
||||
}
|
||||
|
||||
if team.Name == "" {
|
||||
team.Name = extractRepoName(tmp)
|
||||
}
|
||||
if team.Description == "" {
|
||||
team.Description = "团队描述"
|
||||
}
|
||||
|
||||
return team
|
||||
}
|
||||
|
||||
@ -36,6 +36,8 @@ type Config struct {
|
||||
Type RoomType `yaml:"type"`
|
||||
Master string `yaml:"master"` // agent name
|
||||
Members []string `yaml:"members"` // agent names
|
||||
Color string `yaml:"color"` // avatar color
|
||||
Team string `yaml:"team"` // installed team name
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
@ -90,16 +92,19 @@ func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
|
||||
|
||||
r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent)}
|
||||
|
||||
r.master, err = agent.Load(filepath.Join(agentsDir, cfg.Master))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
|
||||
}
|
||||
for _, name := range cfg.Members {
|
||||
a, err := agent.Load(filepath.Join(agentsDir, name))
|
||||
if cfg.Master != "" {
|
||||
agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master)
|
||||
r.master, err = agent.Load(agentPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load member %s: %w", name, err)
|
||||
return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
|
||||
}
|
||||
for _, name := range cfg.Members {
|
||||
a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load member %s: %w", name, err)
|
||||
}
|
||||
r.members[name] = a
|
||||
}
|
||||
r.members[name] = a
|
||||
}
|
||||
|
||||
r.skillMeta, _ = skill.Discover(skillsDir)
|
||||
@ -140,6 +145,9 @@ func (r *Room) Handle(ctx context.Context, userMsg string) error {
|
||||
|
||||
// HandleUserMessage processes a user message with a specific user name.
|
||||
func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error {
|
||||
if r.master == nil {
|
||||
return fmt.Errorf("room has no master agent configured")
|
||||
}
|
||||
r.AppendHistory("user", userName, userMsg)
|
||||
r.setStatus(StatusThinking, "", "")
|
||||
|
||||
@ -319,3 +327,14 @@ func parseRoomConfig(data []byte) (Config, error) {
|
||||
}
|
||||
return cfg, yaml.Unmarshal(parts[1], &cfg)
|
||||
}
|
||||
|
||||
// resolveAgentPath finds agent dir: prefers agentsDir/team/name, falls back to agentsDir/name
|
||||
func resolveAgentPath(agentsDir, team, name string) string {
|
||||
if team != "" {
|
||||
p := filepath.Join(agentsDir, team, name)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return filepath.Join(agentsDir, name)
|
||||
}
|
||||
|
||||
72
progress.md
Normal file
72
progress.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Project: Agent Team
|
||||
Last updated: 2026-03-05
|
||||
|
||||
## Pinned (关键约束)
|
||||
- 一切皆 MD — agent 配置、soul、memory、tasks、history 全部是 Markdown 文件
|
||||
- 使用前必须设置环境变量 `DEEPSEEK_API_KEY` 或其他 LLM provider 的 API key
|
||||
|
||||
## Decisions (决策)
|
||||
- 2026-03-05:使用 Go + Echo + go-openai 作为后端技术栈
|
||||
- 2026-03-05:使用 React 19 + TypeScript + Tailwind v4 + Zustand 作为前端技术栈
|
||||
- 2026-03-05:采用纯文件系统存储,无数据库
|
||||
|
||||
## Constraints (约束)
|
||||
- 暂无
|
||||
|
||||
## TODO
|
||||
### In Progress
|
||||
- 暂无
|
||||
### Pending
|
||||
- 暂无
|
||||
|
||||
## Done
|
||||
- 2026-03-05:支持从 TEAM.md 读取团队元信息
|
||||
- 修改 internal/hub/hub.go,添加 readTeamMeta 函数
|
||||
- 安装团队时从 TEAM.md 解析 name/description/author
|
||||
- 代码变更:`files changed: 1`, `insertions: +29`, `deletions: -4`
|
||||
- Evidence: internal/hub/hub.go:129-175
|
||||
- 2026-03-05:配置精选团队为营销团队和律师团队
|
||||
- 代码变更:`files changed: 1`, `insertions: +20`, `deletions: -51`
|
||||
- Evidence: web/src/components/MarketPage.tsx:18-37
|
||||
- 2026-03-05:创建营销团队 (marketing-team) 并提交到 Gitea
|
||||
- 参考 legal-team 律师团队结构
|
||||
- 包含 3 个 Agent:营销总监(master)、市场分析师(member)、内容策划师(member)
|
||||
- 提交地址:https://gitea.catter.cn/agent-teams/marketing-team.git
|
||||
- Evidence: agents/marketing-team/
|
||||
- 2026-03-05:修复后端 loadTeam 函数解析 skills 错误添加到 agents 的问题
|
||||
- 代码变更:`files changed: 1`, `insertions: +16`, `deletions: -2`
|
||||
- Evidence: internal/api/server.go:648-672
|
||||
- 2026-03-05:修复精选团队和我的团队 grid 风格不一致
|
||||
- 代码变更:`files changed: 1`, `insertions: +26`, `deletions: -8`
|
||||
- Evidence: web/src/components/MarketPage.tsx:388-432
|
||||
- 2026-03-05:替换 MDEditor 为简洁的 GitHub 风格 Markdown 编辑器
|
||||
- 代码变更:`files changed: 4`, `insertions: +68`, `deletions: -45`
|
||||
- Evidence: web/src/components/MarkdownEditor.tsx (new), TeamDetail.tsx, UserSettings.tsx, Onboarding.tsx
|
||||
- 2026-03-05:精选团队查看详情改为打开 GitHub 链接,我的团队查看详情打开本地详情
|
||||
- 代码变更:`files changed: 2`, `insertions: +30`, `deletions: -15`
|
||||
- Evidence: MarketPage.tsx, TeamDetail.tsx
|
||||
- 2026-03-05:精选团队添加"立即雇佣"按钮
|
||||
- 代码变更:`files changed: 1`, `insertions: +25`, `deletions: -10`
|
||||
- Evidence: MarketPage.tsx
|
||||
- 2026-03-05:侧边栏布局改为 Discord 风格 - 聊天按钮在上,工具栏(市场/主题/设置)在下
|
||||
- 代码变更:`files changed: 1`, `insertions: +80`, `deletions: -20`
|
||||
- Evidence: App.tsx
|
||||
- 2026-03-05:顶部 Logo 改为 Sparkles AI 图标,移除背景色
|
||||
- 代码变更:`files changed: 1`, `insertions: +5`, `deletions: -3`
|
||||
- Evidence: App.tsx
|
||||
- 2026-03-05:实施 Discord 风格三栏布局 - 左侧群列表 + 中间成员列表 + 聊天区域 + 右侧工作区
|
||||
- 新建 MemberList.tsx、RightSidebar.tsx
|
||||
- 修改 ChatView.tsx 移除内置右侧栏
|
||||
- 代码变更:`files changed: 5`, `insertions: +350`, `deletions: -200`
|
||||
- Evidence: App.tsx, MemberList.tsx (new), RightSidebar.tsx (new), ChatView.tsx
|
||||
- 2026-03-05:完成群聊创建和实时对话功能
|
||||
- 支持创建新群聊,WebSocket 实时消息推送
|
||||
- 后端房间管理优化,支持任务和工作区文件同步
|
||||
- 代码变更:`files changed: 4`, `insertions: +200`, `deletions: -50`
|
||||
- Evidence: internal/room/room.go, internal/api/server.go, web/src/store.ts, web/src/types.ts
|
||||
|
||||
## Experiments
|
||||
- 暂无
|
||||
|
||||
## Notes
|
||||
- 2026-03-05:⚠️ 启动前必须设置环境变量 `export DEEPSEEK_API_KEY=your_key_here` 或其他 provider (OPENAI_API_KEY, KIMI_API_KEY, OLLAMA_HOST)
|
||||
@ -1,7 +1,7 @@
|
||||
name: legal-team
|
||||
description: 团队描述
|
||||
author:
|
||||
repo_url: https://gitea.catter.cn/agent-teams/legal-team.git
|
||||
name: 法律咨询团队
|
||||
description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
|
||||
author: Agent Team
|
||||
repo_url: https://gitea.catter.cn/agent-teams/legal-team
|
||||
agents:
|
||||
- 合同律师
|
||||
- 合规专员
|
||||
|
||||
9
teams/marketing-team/team.yaml
Normal file
9
teams/marketing-team/team.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
name: 营销团队
|
||||
description: 营销团队,包含市场分析、竞品研究和内容策划
|
||||
author: Agent Team
|
||||
repo_url: https://gitea.catter.cn/agent-teams/marketing-team
|
||||
agents:
|
||||
[]
|
||||
skills:
|
||||
[]
|
||||
installed_at: 2026-03-05
|
||||
@ -1,9 +1,14 @@
|
||||
# 我的简介
|
||||
|
||||
## 我是谁
|
||||
(介绍你的身份、职业背景)
|
||||
我是[
|
||||
|
||||
## 工作风格
|
||||
- 喜欢的沟通方式
|
||||
- 重视的点
|
||||
- 不喜欢的内容
|
||||
- 喜欢[沟通方式]
|
||||
- 重视[重视的点]
|
||||
- 不喜欢[不喜欢的]
|
||||
|
||||
## 期望的回复方式
|
||||
- 用 bullet point 列出要点
|
||||
- 重要的决定给出 pros 和 cons
|
||||
- 提供数据支持
|
||||
@ -1,8 +1,11 @@
|
||||
---
|
||||
name: 用户
|
||||
description:
|
||||
name: 第五季
|
||||
description: 产品经理,擅长需求分析
|
||||
provider: deepseek
|
||||
model: deepseek-chat
|
||||
api_key_env: DEEPSEEK_API_KEY
|
||||
avatar_color: "#5865F2"
|
||||
avatar_color: '#5865F2'
|
||||
preferences:
|
||||
language: ""
|
||||
tone: ""
|
||||
---
|
||||
|
||||
191
web/src/App.tsx
191
web/src/App.tsx
@ -1,20 +1,29 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MessageSquare, ShoppingCart, Hash, Settings } from 'lucide-react'
|
||||
import { ShoppingCart, Sparkles, Settings, Plus } from 'lucide-react'
|
||||
import { useStore } from './store'
|
||||
import { RoomSidebar } from './components/RoomSidebar'
|
||||
import { ChatView } from './components/ChatView'
|
||||
import { MarketPage } from './components/MarketPage'
|
||||
import { UserSettings } from './components/UserSettings'
|
||||
import { ThemeToggle } from './components/ThemeToggle'
|
||||
import { Onboarding } from './components/Onboarding'
|
||||
import { MemberList } from './components/MemberList'
|
||||
import { RightSidebar } from './components/RightSidebar'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
const COLORS = ['#5865F2', '#57F287', '#FEE75C', '#EB459E', '#ED4245', '#3BA55C', '#FAA61A', '#00B0F4']
|
||||
const randomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]
|
||||
|
||||
export default function App() {
|
||||
const { page, setPage, onboardingCompleted, setOnboardingCompleted, fetchUser } = useStore()
|
||||
const { page, setPage, onboardingCompleted, setOnboardingCompleted, fetchUser, fetchRooms, rooms, activeRoomId, setActiveRoom } = useStore()
|
||||
const [showOnboarding, setShowOnboarding] = useState(onboardingCompleted ? false : true)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [newRoomName, setNewRoomName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser()
|
||||
}, [fetchUser])
|
||||
fetchRooms()
|
||||
}, [fetchUser, fetchRooms])
|
||||
|
||||
const handleOnboardingComplete = () => {
|
||||
setShowOnboarding(false)
|
||||
@ -22,10 +31,17 @@ export default function App() {
|
||||
fetchUser()
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ id: 'chat', icon: MessageSquare, label: '群聊' },
|
||||
{ id: 'market', icon: ShoppingCart, label: '市场' },
|
||||
] as const
|
||||
const createRoom = async () => {
|
||||
if (!newRoomName.trim()) return
|
||||
const color = randomColor()
|
||||
await fetch(`${API}/rooms`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newRoomName.trim(), type: 'dept', color, master: '', members: [] })
|
||||
})
|
||||
setCreating(false)
|
||||
setNewRoomName('')
|
||||
useStore.getState().fetchRooms()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -37,75 +53,120 @@ export default function App() {
|
||||
{/* Left sidebar - Navigation */}
|
||||
<div className="flex flex-col w-[72px] bg-[var(--sidebar-bg)] items-center py-3 gap-2">
|
||||
{/* Logo */}
|
||||
<div className="w-12 h-12 bg-[var(--accent)] rounded-xl flex items-center justify-center mb-2">
|
||||
<Hash className="w-6 h-6 text-white" />
|
||||
<div className="w-12 h-12 rounded-xl flex items-center justify-center mb-2">
|
||||
<Sparkles className="w-7 h-7 text-[var(--accent)]" />
|
||||
</div>
|
||||
|
||||
{/* Nav buttons */}
|
||||
<div className="flex flex-col gap-1 w-full px-3">
|
||||
{navItems.map(item => (
|
||||
<NavBtn
|
||||
key={item.id}
|
||||
icon={<item.icon className="w-5 h-5" />}
|
||||
label={item.label}
|
||||
active={page === item.id}
|
||||
onClick={() => setPage(item.id as typeof page)}
|
||||
/>
|
||||
))}
|
||||
{/* Room list */}
|
||||
<div className="flex flex-col gap-1 w-full px-3 flex-1 overflow-y-auto scrollbar-thin">
|
||||
{rooms.map(r => {
|
||||
const isActive = activeRoomId === r.id
|
||||
const initial = r.name[0]?.toUpperCase() || '?'
|
||||
const color = r.color || '#5865F2'
|
||||
return (
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => { setPage('chat'); setActiveRoom(r.id) }}
|
||||
title={r.name}
|
||||
className={`
|
||||
w-full h-10 rounded-lg flex items-center justify-center transition-all duration-200 relative text-white text-sm font-bold
|
||||
${isActive ? 'ring-2 ring-white/40' : 'hover:rounded-xl'}
|
||||
`}
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{initial}
|
||||
{r.status !== 'pending' && (
|
||||
<span className={`absolute top-0.5 right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[var(--sidebar-bg)] ${r.status === 'thinking' ? 'bg-yellow-400' : 'bg-green-400 animate-pulse'}`} />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Add room button */}
|
||||
<button
|
||||
onClick={() => setCreating(true)}
|
||||
title="创建群聊"
|
||||
className="w-full h-10 rounded-lg flex items-center justify-center text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-all duration-200"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="w-8 h-[2px] bg-[var(--border)] rounded-full my-1" />
|
||||
{/* Bottom buttons */}
|
||||
<div className="mt-auto flex flex-col gap-1 w-full px-3">
|
||||
{/* Market */}
|
||||
<button
|
||||
onClick={() => setPage('market')}
|
||||
className={`
|
||||
w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
${page === 'market' ? 'bg-[var(--accent)] text-white' : 'hover:bg-[var(--bg-hover)]'}
|
||||
`}
|
||||
title="市场"
|
||||
>
|
||||
<ShoppingCart className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<ThemeToggle />
|
||||
{/* Theme toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setPage('settings')}
|
||||
className={`
|
||||
w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
${page === 'settings' ? 'bg-[var(--accent)] text-white' : 'hover:bg-[var(--bg-hover)]'}
|
||||
`}
|
||||
title="设置"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
</button>
|
||||
{/* Settings */}
|
||||
<button
|
||||
onClick={() => setPage('settings')}
|
||||
className={`
|
||||
w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200
|
||||
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
|
||||
${page === 'settings' ? 'bg-[var(--accent)] text-white' : 'hover:bg-[var(--bg-hover)]'}
|
||||
`}
|
||||
title="设置"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create room modal */}
|
||||
{creating && (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={() => setCreating(false)}>
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 w-80 shadow-xl" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="font-semibold mb-3">创建新群聊</h3>
|
||||
<input
|
||||
autoFocus
|
||||
className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)] mb-3"
|
||||
placeholder="群聊名称"
|
||||
value={newRoomName}
|
||||
onChange={e => setNewRoomName(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && createRoom()}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={createRoom}
|
||||
className="flex-1 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white rounded-lg py-2 text-sm font-medium"
|
||||
>
|
||||
创建
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCreating(false)}
|
||||
className="flex-1 bg-[var(--bg-hover)] hover:bg-[var(--bg-active)] rounded-lg py-2 text-sm"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
{page === 'chat' && <RoomSidebar />}
|
||||
{page === 'chat' && <ChatView />}
|
||||
{page === 'chat' && (
|
||||
<>
|
||||
<MemberList />
|
||||
<ChatView />
|
||||
<RightSidebar />
|
||||
</>
|
||||
)}
|
||||
{page === 'market' && <MarketPage />}
|
||||
{page === 'settings' && <UserSettings />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function NavBtn({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
title={label}
|
||||
className={`
|
||||
w-full h-12 rounded-xl flex items-center justify-center transition-all duration-200 group relative
|
||||
${active
|
||||
? 'bg-[var(--accent)] text-white'
|
||||
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{icon}
|
||||
{/* Active indicator */}
|
||||
{active && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white rounded-r-full" />
|
||||
)}
|
||||
{/* Hover tooltip */}
|
||||
<div className="absolute left-full ml-4 px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-primary)] text-sm rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50 shadow-lg">
|
||||
{label}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@ -3,38 +3,23 @@ import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import {
|
||||
Hash,
|
||||
Users,
|
||||
ListTodo,
|
||||
FileText,
|
||||
X,
|
||||
Crown,
|
||||
Plus,
|
||||
Smile,
|
||||
Send,
|
||||
Crown,
|
||||
} from 'lucide-react'
|
||||
import { useStore } from '../store'
|
||||
import type { Message } from '../types'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
export function ChatView() {
|
||||
const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore()
|
||||
const { activeRoomId, rooms, messages, sendMessage, user } = useStore()
|
||||
const [input, setInput] = useState('')
|
||||
const [drawer, setDrawer] = useState<null | 'members' | 'tasks' | 'files'>(null)
|
||||
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const room = rooms.find(r => r.id === activeRoomId)
|
||||
const msgs: Message[] = activeRoomId ? (messages[activeRoomId] || []) : []
|
||||
const tasksMd = activeRoomId ? (tasks[activeRoomId] || '') : ''
|
||||
const files = activeRoomId ? (workspace[activeRoomId] || []) : []
|
||||
|
||||
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs])
|
||||
|
||||
const openFile = async (filename: string) => {
|
||||
const d = await fetch(`${API}/rooms/${activeRoomId}/workspace/${filename}`).then(r => r.json())
|
||||
setPreviewFile({ name: filename, content: d.content || '' })
|
||||
}
|
||||
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs, room?.status])
|
||||
|
||||
if (!room) {
|
||||
return (
|
||||
@ -45,16 +30,10 @@ export function ChatView() {
|
||||
)
|
||||
}
|
||||
|
||||
const statusLabel = room.status === 'working' && room.activeAgent
|
||||
const statusLabel = room.status === 'working' && room.activeAgent
|
||||
? `${room.activeAgent} ${room.action || ''}`
|
||||
: room.status === 'thinking' ? '思考中...' : room.status === 'working' ? '工作中...' : '空闲'
|
||||
|
||||
const drawerButtons = [
|
||||
{ id: 'members', icon: Users, label: '成员' },
|
||||
{ id: 'tasks', icon: ListTodo, label: '任务' },
|
||||
{ id: 'files', icon: FileText, label: '产物' },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 overflow-hidden bg-[var(--bg-primary)]">
|
||||
{/* Main chat area */}
|
||||
@ -94,7 +73,11 @@ export function ChatView() {
|
||||
<p className="text-xs mt-1">发送消息开始对话</p>
|
||||
</div>
|
||||
)}
|
||||
{msgs.map(msg => <MessageBubble key={msg.id} msg={msg} />)}
|
||||
{msgs.map(msg => <MessageBubble key={msg.id} msg={msg} userName={user?.name} />)}
|
||||
{/* Thinking indicator */}
|
||||
{room.status === 'thinking' && msgs[msgs.length - 1]?.role === 'user' && (
|
||||
<ThinkingBubble agent={room.master} />
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
@ -134,124 +117,14 @@ export function ChatView() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right panel */}
|
||||
<div className="w-72 border-l border-[var(--border)] bg-[var(--bg-secondary)] flex flex-col overflow-hidden">
|
||||
{/* Panel tabs */}
|
||||
<div className="flex border-b border-[var(--border)]">
|
||||
{drawerButtons.map(btn => (
|
||||
<button
|
||||
key={btn.id}
|
||||
onClick={() => setDrawer(drawer === btn.id ? null : btn.id)}
|
||||
className={`
|
||||
flex-1 py-3 text-xs font-medium flex items-center justify-center gap-1.5 transition-colors border-b-2
|
||||
${drawer === btn.id
|
||||
? 'border-[var(--accent)] text-[var(--accent)]'
|
||||
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<btn.icon className="w-4 h-4" />
|
||||
{btn.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Panel content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
|
||||
{/* Members */}
|
||||
{(drawer === 'members' || drawer === null) && (
|
||||
<section className={drawer !== 'members' && drawer !== null ? 'hidden' : ''}>
|
||||
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
成员 — {1 + msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).length}
|
||||
</h3>
|
||||
<MemberItem
|
||||
name={room.master}
|
||||
role="master"
|
||||
status={room.status === 'thinking' ? 'thinking' : 'idle'}
|
||||
/>
|
||||
{msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => (
|
||||
<MemberItem
|
||||
key={name}
|
||||
name={name}
|
||||
role="member"
|
||||
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'idle'}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Tasks */}
|
||||
{(drawer === 'tasks' || drawer === null) && tasksMd && (
|
||||
<section className={`mt-4 ${drawer !== 'tasks' && drawer !== null ? 'hidden' : ''}`}>
|
||||
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
任务列表
|
||||
</h3>
|
||||
<div className="text-sm prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Files */}
|
||||
{(drawer === 'files' || drawer === null) && files.length > 0 && (
|
||||
<section className={`mt-4 ${drawer !== 'files' && drawer !== null ? 'hidden' : ''}`}>
|
||||
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
|
||||
<FileText className="w-3 h-3" />
|
||||
产物文件
|
||||
</h3>
|
||||
{files.map(f => (
|
||||
<div
|
||||
key={f}
|
||||
onClick={() => openFile(f)}
|
||||
className="flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-hover)] cursor-pointer py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="truncate">{f}</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File preview modal */}
|
||||
{previewFile && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setPreviewFile(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col shadow-2xl"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-[var(--accent)]" />
|
||||
<span className="font-semibold">{previewFile.name}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPreviewFile(null)}
|
||||
className="w-8 h-8 rounded flex items-center justify-center text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-4 prose dark:prose-invert max-w-none text-sm">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MessageBubble({ msg }: { msg: Message }) {
|
||||
function MessageBubble({ msg, userName }: { msg: Message; userName?: string }) {
|
||||
const isUser = msg.role === 'user'
|
||||
const isMaster = msg.role === 'master'
|
||||
const agentName = msg.agent || 'Unknown'
|
||||
|
||||
const avatarColors = [
|
||||
'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500',
|
||||
@ -259,41 +132,38 @@ function MessageBubble({ msg }: { msg: Message }) {
|
||||
'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500',
|
||||
'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500', 'bg-rose-500'
|
||||
]
|
||||
const colorIndex = msg.agent.charCodeAt(0) % avatarColors.length
|
||||
const colorIndex = agentName.charCodeAt(0) % avatarColors.length
|
||||
const displayName = isUser ? (userName || agentName) : agentName
|
||||
|
||||
return (
|
||||
<div className={`flex gap-3 group ${isUser ? 'flex-row-reverse' : ''}`}>
|
||||
{/* Avatar */}
|
||||
{!isUser && (
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 mt-0.5 ${avatarColors[colorIndex]}`}>
|
||||
{isMaster ? <Crown className="w-5 h-5" /> : msg.agent[0]?.toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 mt-0.5 ${isUser ? 'bg-[var(--accent)]' : avatarColors[colorIndex]}`}>
|
||||
{isMaster ? <Crown className="w-5 h-5" /> : displayName[0]?.toUpperCase()}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={`flex flex-col max-w-[70%] ${isUser ? 'items-end' : 'items-start'}`}>
|
||||
{/* Name + timestamp */}
|
||||
{!isUser && (
|
||||
<div className="flex items-center gap-2 mb-0.5">
|
||||
<span className={`font-medium text-sm ${isMaster ? 'text-[var(--color-warning)]' : 'text-[var(--text-primary)]'}`}>
|
||||
{msg.agent}
|
||||
{/* Name */}
|
||||
<div className={`flex items-center gap-2 mb-0.5 ${isUser ? 'flex-row-reverse' : ''}`}>
|
||||
<span className={`font-medium text-sm ${isMaster ? 'text-[var(--color-warning)]' : isUser ? 'text-[var(--accent)]' : 'text-[var(--text-primary)]'}`}>
|
||||
{displayName}
|
||||
</span>
|
||||
{isMaster && (
|
||||
<span className="text-[10px] px-1 py-0.5 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded font-medium">
|
||||
MASTER
|
||||
</span>
|
||||
{isMaster && (
|
||||
<span className="text-[10px] px-1 py-0.5 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded font-medium">
|
||||
MASTER
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message bubble */}
|
||||
<div
|
||||
className={`
|
||||
px-3.5 py-2 rounded-lg text-sm relative group
|
||||
px-3.5 py-2 rounded-lg text-sm relative
|
||||
${isUser
|
||||
? 'bg-[var(--accent)] text-white rounded-tr-sm'
|
||||
: isMaster
|
||||
? 'bg-[var(--bg-tertiary)] border-2 border-[var(--color-warning)]/30 rounded-tl-sm'
|
||||
? 'bg-[var(--bg-tertiary)] border-l-4 border-[var(--color-warning)] rounded-tl-sm'
|
||||
: 'bg-[var(--bg-tertiary)] rounded-tl-sm'
|
||||
}
|
||||
`}
|
||||
@ -310,37 +180,20 @@ function MessageBubble({ msg }: { msg: Message }) {
|
||||
)
|
||||
}
|
||||
|
||||
function MemberItem({ name, role, status }: { name: string; role: string; status: string }) {
|
||||
const dotClass = status === 'thinking'
|
||||
? 'bg-[var(--color-warning)] animate-pulse'
|
||||
: status === 'working'
|
||||
? 'bg-[var(--color-success)] animate-pulse'
|
||||
: 'bg-[var(--text-muted)]'
|
||||
|
||||
const statusText = status === 'thinking' ? '思考中' : status === 'working' ? '工作中' : '空闲'
|
||||
|
||||
function ThinkingBubble({ agent }: { agent: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors group">
|
||||
<div className="relative">
|
||||
<div className={`w-7 h-7 rounded-full bg-[var(--accent)] flex items-center justify-center text-white text-xs font-semibold`}>
|
||||
{role === 'master' ? <Crown className="w-3.5 h-3.5" /> : name[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[var(--bg-secondary)] ${dotClass}`} />
|
||||
<div className="flex gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[var(--color-warning)]/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<Crown className="w-5 h-5 text-[var(--color-warning)]" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{role === 'master' && <Crown className="w-3 h-3 text-[var(--color-warning)]" />}
|
||||
<span className="text-sm font-medium truncate">{name}</span>
|
||||
{role === 'master' && (
|
||||
<span className="text-[10px] px-1 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded">
|
||||
MASTER
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium text-sm text-[var(--color-warning)] mb-0.5">{agent || 'Master'}</span>
|
||||
<div className="px-3.5 py-2.5 rounded-lg bg-[var(--bg-tertiary)] border-l-4 border-[var(--color-warning)] rounded-tl-sm flex items-center gap-1.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
59
web/src/components/MarkdownEditor.tsx
Normal file
59
web/src/components/MarkdownEditor.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Eye, Edit2 } from 'lucide-react'
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
height?: number
|
||||
}
|
||||
|
||||
export function MarkdownEditor({ value, onChange, height = 250 }: MarkdownEditorProps) {
|
||||
const [mode, setMode] = useState<'edit' | 'preview'>('edit')
|
||||
|
||||
return (
|
||||
<div className="border border-[var(--border)] rounded-lg overflow-hidden" style={{ height }}>
|
||||
<div className="flex items-center gap-1 px-3 py-2 bg-[var(--bg-secondary)] border-b border-[var(--border)]">
|
||||
<button
|
||||
onClick={() => setMode('edit')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded text-xs transition-colors ${
|
||||
mode === 'edit'
|
||||
? 'bg-[var(--accent)] text-white'
|
||||
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<Edit2 className="w-3.5 h-3.5" />
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('preview')}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded text-xs transition-colors ${
|
||||
mode === 'preview'
|
||||
? 'bg-[var(--accent)] text-white'
|
||||
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
预览
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-[calc(100%-40px)] overflow-y-auto">
|
||||
{mode === 'edit' ? (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="w-full h-full p-4 bg-[var(--bg-tertiary)] text-[var(--text-primary)] outline-none resize-none font-mono text-sm"
|
||||
style={{ height: height - 40 }}
|
||||
/>
|
||||
) : (
|
||||
<div className="p-4 prose prose-invert prose-sm max-w-none bg-[var(--bg-tertiary)]">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{value || '*无内容*'}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ShoppingCart, Github, Upload, Users, ArrowRight, Check, AlertCircle, Package, Zap, Star, Globe, Link as LinkIcon } from 'lucide-react'
|
||||
import { ShoppingCart, Github, Upload, Users, ArrowRight, Check, AlertCircle, Package, Zap, Star, Globe, Link as LinkIcon, Loader2 } from 'lucide-react'
|
||||
import { useStore } from '../store'
|
||||
import { TeamDetail } from './TeamDetail'
|
||||
|
||||
@ -18,56 +18,20 @@ interface Team {
|
||||
const PRESET_TEAMS: Team[] = [
|
||||
{
|
||||
name: '法律咨询团队',
|
||||
description: '法律咨询团队,包含合同审查和法律风险评估',
|
||||
description: '法律咨询团队,包含合同审查、法律风险评估和合规管理',
|
||||
author: 'Agent Team',
|
||||
repo_url: 'https://github.com/sdaduanbilei/legal-team',
|
||||
repo_url: 'https://gitea.catter.cn/agent-teams/legal-team',
|
||||
agents: ['法律总监', '合同律师', '合规专员'],
|
||||
skills: ['合同审查', '法律知识库'],
|
||||
installed_at: '',
|
||||
},
|
||||
{
|
||||
name: '开发团队',
|
||||
description: '开发团队,包含代码审查、架构设计、全栈测试',
|
||||
author: 'Agent Team',
|
||||
repo_url: 'https://github.com/sdaduanbilei/dev-team',
|
||||
agents: ['技术主管', '代码审查员', '测试工程师'],
|
||||
skills: ['代码扫描', '性能分析'],
|
||||
installed_at: '',
|
||||
},
|
||||
{
|
||||
name: '营销团队',
|
||||
description: '营销团队,包含内容创意、广告投放策略制定',
|
||||
description: '营销团队,包含市场分析、竞品研究和内容策划',
|
||||
author: 'Agent Team',
|
||||
repo_url: 'https://github.com/sdaduanbilei/marketing-team',
|
||||
agents: ['创意总监', '策略分析师', '文案', '运营'],
|
||||
skills: ['数据分析', '创意评估'],
|
||||
installed_at: '',
|
||||
},
|
||||
{
|
||||
name: '医疗咨询',
|
||||
description: '医疗领域专业咨询团队',
|
||||
author: 'Expert AI Corp',
|
||||
repo_url: 'https://github.com/expert-ai/medical-team',
|
||||
agents: ['医疗顾问', '营养师', '健康指导员'],
|
||||
skills: ['症状分析', '饮食建议'],
|
||||
installed_at: '',
|
||||
},
|
||||
{
|
||||
name: '金融投资',
|
||||
description: '金融分析与投资咨询团队',
|
||||
author: 'FiTech Solutions',
|
||||
repo_url: 'https://github.com/fitech/finance-team',
|
||||
agents: ['财务分析师', '投资顾问', '风险测评'],
|
||||
skills: ['股市预测', '财报分析'],
|
||||
installed_at: '',
|
||||
},
|
||||
{
|
||||
name: '心理咨询',
|
||||
description: '专业心理咨询服务团队',
|
||||
author: 'MindWell Inc',
|
||||
repo_url: 'https://github.com/mindwell/psychology-team',
|
||||
agents: ['心理咨询师', '治疗助理', '危机干预'],
|
||||
skills: ['情绪评估', '心理疏导'],
|
||||
repo_url: 'https://gitea.catter.cn/agent-teams/marketing-team',
|
||||
agents: ['营销总监', '市场分析师', '内容策划师'],
|
||||
skills: ['数据分析', '用户画像'],
|
||||
installed_at: '',
|
||||
},
|
||||
]
|
||||
@ -81,6 +45,7 @@ export function MarketPage() {
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [teams, setTeams] = useState<Team[]>([])
|
||||
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null)
|
||||
const [installingTeams, setInstallingTeams] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === 'teams') {
|
||||
@ -94,6 +59,25 @@ export function MarketPage() {
|
||||
setTeams(data || [])
|
||||
}
|
||||
|
||||
const installTeam = async (team: Team) => {
|
||||
if (!team.repo_url || installingTeams.has(team.name)) return
|
||||
setInstallingTeams(prev => new Set(prev).add(team.name))
|
||||
try {
|
||||
await fetch(`${API}/hub/install`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ repo: team.repo_url })
|
||||
})
|
||||
fetchAgents()
|
||||
fetchTeams()
|
||||
} finally {
|
||||
setInstallingTeams(prev => {
|
||||
const next = new Set(prev)
|
||||
next.delete(team.name)
|
||||
return next
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const install = async () => {
|
||||
if (!repo.trim()) return
|
||||
setStatus('loading')
|
||||
@ -149,13 +133,6 @@ export function MarketPage() {
|
||||
fetchTeams()
|
||||
}
|
||||
|
||||
const isSelected = (teamName: string) => {
|
||||
if (selectedTeam) {
|
||||
return selectedTeam.name === teamName
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto bg-[var(--bg-primary)]">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
@ -209,10 +186,10 @@ export function MarketPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{selectedTeam && !isSelected(selectedTeam.name) && (
|
||||
{selectedTeam && (
|
||||
<TeamDetail
|
||||
team={selectedTeam}
|
||||
installed={teams.some(t => t.name === selectedTeam.name)}
|
||||
installed={tab === 'teams' || teams.some(t => t.name === selectedTeam.name)}
|
||||
onBack={() => setSelectedTeam(null)}
|
||||
onInstalled={() => {
|
||||
setSelectedTeam(null)
|
||||
@ -237,13 +214,13 @@ export function MarketPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{PRESET_TEAMS.map(team => {
|
||||
const isInstalled = teams.some(t => t.name === team.name)
|
||||
const isInstalling = installingTeams.has(team.name)
|
||||
return (
|
||||
<div
|
||||
key={team.name}
|
||||
className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all cursor-pointer ${
|
||||
className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all ${
|
||||
isInstalled ? 'ring-2 ring-[var(--accent)] ring-offset-2' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedTeam(team)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
@ -274,17 +251,38 @@ export function MarketPage() {
|
||||
{team.skills?.length || 0} Skills
|
||||
</span>
|
||||
</div>
|
||||
{team.repo_url && (
|
||||
<a
|
||||
href={team.repo_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-[var(--accent)] hover:underline flex items-center gap-1"
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5" />
|
||||
查看详情
|
||||
</a>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{team.repo_url && (
|
||||
<a
|
||||
href={team.repo_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-[var(--accent)] hover:underline flex items-center gap-1"
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5" />
|
||||
查看详情
|
||||
</a>
|
||||
)}
|
||||
{!isInstalled && team.repo_url && (
|
||||
<button
|
||||
onClick={() => installTeam(team)}
|
||||
disabled={isInstalling}
|
||||
className="ml-auto flex items-center gap-1.5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] disabled:opacity-50 px-3 py-1.5 rounded-lg text-xs font-medium text-white transition-colors"
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
安装中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShoppingCart className="w-3.5 h-3.5" />
|
||||
立即雇佣
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -405,23 +403,25 @@ export function MarketPage() {
|
||||
<div
|
||||
key={team.name}
|
||||
onClick={() => setSelectedTeam(team)}
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 cursor-pointer hover:border-[var(--accent)] transition-colors"
|
||||
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all cursor-pointer ring-2 ring-[var(--accent)] ring-offset-2"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="font-semibold">{team.name}</h3>
|
||||
<ArrowRight className="w-4 h-4 text-[var(--text-muted)]" />
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1">{team.name}</h3>
|
||||
<p className="text-xs text-[var(--text-muted)]">{team.description || '暂无描述'}</p>
|
||||
</div>
|
||||
<div className="w-8 h-8 bg-[var(--color-success)] rounded-full flex items-center justify-center text-white">
|
||||
<Check className="w-4 h-4" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-muted)] mb-3 line-clamp-2">
|
||||
{team.description || '暂无描述'}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)]">
|
||||
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)] mb-3">
|
||||
<span className="flex items-center gap-1">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
{team.agents?.length || 0} agents
|
||||
{team.agents?.length || 0} 成员
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Package className="w-3.5 h-3.5" />
|
||||
{team.skills?.length || 0} skills
|
||||
{team.skills?.length || 0} Skills
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
139
web/src/components/MemberList.tsx
Normal file
139
web/src/components/MemberList.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Crown, Users } from 'lucide-react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
interface TeamInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export function MemberList() {
|
||||
const { activeRoomId, rooms, setRoomTeam } = useStore()
|
||||
const [teams, setTeams] = useState<TeamInfo[]>([])
|
||||
const [loadingTeam, setLoadingTeam] = useState(false)
|
||||
|
||||
const room = rooms.find(r => r.id === activeRoomId)
|
||||
|
||||
useEffect(() => {
|
||||
if (room && !room.team) {
|
||||
fetch(`${API}/teams`).then(r => r.json()).then(data => setTeams(data || []))
|
||||
}
|
||||
}, [room?.id, room?.team])
|
||||
|
||||
if (!room) {
|
||||
return (
|
||||
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col">
|
||||
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
|
||||
<span className="text-sm font-semibold text-[var(--text-muted)]">成员</span>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center text-[var(--text-muted)] text-xs">
|
||||
选择群聊查看成员
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!room.team) {
|
||||
return (
|
||||
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col">
|
||||
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
|
||||
<span className="text-sm font-semibold text-[var(--text-muted)]">成员</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 px-4">
|
||||
<Users className="w-8 h-8 text-[var(--text-muted)]" />
|
||||
<p className="text-xs text-[var(--text-muted)] text-center">选择一个团队开始工作</p>
|
||||
<div className="w-full flex flex-col gap-1.5">
|
||||
{teams.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
disabled={loadingTeam}
|
||||
onClick={async () => {
|
||||
setLoadingTeam(true)
|
||||
await setRoomTeam(room.id, t.id)
|
||||
setLoadingTeam(false)
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 rounded-lg bg-[var(--bg-hover)] hover:bg-[var(--bg-active)] transition-colors"
|
||||
>
|
||||
<div className="text-xs font-medium truncate">{t.name}</div>
|
||||
{t.description && <div className="text-[10px] text-[var(--text-muted)] truncate mt-0.5">{t.description}</div>}
|
||||
</button>
|
||||
))}
|
||||
{teams.length === 0 && (
|
||||
<p className="text-[10px] text-[var(--text-muted)] text-center">暂无已安装团队</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const memberAgents = room.members || []
|
||||
|
||||
return (
|
||||
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col overflow-hidden">
|
||||
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-[var(--text-muted)]">成员 — {1 + memberAgents.length}</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch(`${API}/teams`).then(r => r.json()).then(data => setTeams(data || []))
|
||||
}}
|
||||
title="切换团队"
|
||||
className="text-[10px] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
{room.team}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-2 scrollbar-thin">
|
||||
<div className="px-2">
|
||||
<MemberItem
|
||||
name={room.master}
|
||||
role="master"
|
||||
status={room.status === 'thinking' ? 'thinking' : 'idle'}
|
||||
/>
|
||||
{memberAgents.map(name => (
|
||||
<MemberItem
|
||||
key={name}
|
||||
name={name}
|
||||
role="member"
|
||||
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'idle'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MemberItem({ name, role, status }: { name: string; role: string; status: string }) {
|
||||
const dotClass = status === 'thinking'
|
||||
? 'bg-[var(--color-warning)] animate-pulse'
|
||||
: status === 'working'
|
||||
? 'bg-[var(--color-success)] animate-pulse'
|
||||
: 'bg-[var(--text-muted)]'
|
||||
|
||||
const statusText = status === 'thinking' ? '思考中' : status === 'working' ? '工作中' : '空闲'
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors group mb-0.5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-[var(--accent)] flex items-center justify-center text-white text-xs font-semibold">
|
||||
{role === 'master' ? <Crown className="w-4 h-4" /> : name[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[var(--channel-list-bg)] ${dotClass}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1">
|
||||
{role === 'master' && <Crown className="w-3 h-3 text-[var(--color-warning)] flex-shrink-0" />}
|
||||
<span className="text-sm font-medium truncate">{name}</span>
|
||||
</div>
|
||||
<span className="text-[10px] text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { ChevronRight, ChevronLeft, Check, Hash, Sparkles, Key, User } from 'lucide-react'
|
||||
import MDEditor from '@uiw/react-md-editor'
|
||||
import { MarkdownEditor } from './MarkdownEditor'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
@ -211,9 +211,9 @@ export function Onboarding({ onComplete }: OnboardingProps) {
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">详细简介(支持 Markdown)</label>
|
||||
<div className="border border-[var(--border)] rounded-lg overflow-hidden bg-[var(--bg-tertiary)]">
|
||||
<MDEditor
|
||||
value={profile || `# 我的简介
|
||||
<div className="mt-2">
|
||||
<MarkdownEditor
|
||||
value={profile || `# 我的简介
|
||||
|
||||
## 我是谁
|
||||
我是[你的名字],[职业/背景]。
|
||||
@ -227,26 +227,10 @@ export function Onboarding({ onComplete }: OnboardingProps) {
|
||||
- 用 bullet point 列出要点
|
||||
- 重要的决定给出 pros 和 cons
|
||||
- 提供数据支持`}
|
||||
onChange={v => setProfile(v || '')}
|
||||
height={250}
|
||||
preview="edit"
|
||||
visibleDragbar={false}
|
||||
className="mt-2"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
}}
|
||||
textareaProps={{
|
||||
placeholder: '详细编辑你的个人简介...',
|
||||
style: {
|
||||
fontFamily: 'var(--font-mono), monospace',
|
||||
fontSize: '13px',
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
padding: '1rem',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
onChange={(v: string) => setProfile(v)}
|
||||
height={250}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
119
web/src/components/RightSidebar.tsx
Normal file
119
web/src/components/RightSidebar.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { ListTodo, FileText, X, ExternalLink } from 'lucide-react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
export function RightSidebar() {
|
||||
const { activeRoomId, tasks, workspace } = useStore()
|
||||
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
|
||||
|
||||
const tasksMd = activeRoomId ? (tasks[activeRoomId] || '') : ''
|
||||
const files = activeRoomId ? (workspace[activeRoomId] || []) : []
|
||||
|
||||
const openFile = async (filename: string) => {
|
||||
if (!activeRoomId) return
|
||||
const d = await fetch(`${API}/rooms/${activeRoomId}/workspace/${filename}`).then(r => r.json())
|
||||
setPreviewFile({ name: filename, content: d.content || '' })
|
||||
}
|
||||
|
||||
if (!activeRoomId) {
|
||||
return (
|
||||
<div className="w-[280px] bg-[var(--bg-secondary)] border-l border-[var(--border)] flex flex-col">
|
||||
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
|
||||
<span className="text-sm font-semibold text-[var(--text-muted)]">工作区</span>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center text-[var(--text-muted)] text-xs">
|
||||
选择群聊查看工作区
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-[280px] bg-[var(--bg-secondary)] border-l border-[var(--border)] flex flex-col overflow-hidden">
|
||||
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
|
||||
<span className="text-sm font-semibold text-[var(--text-muted)]">工作区</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin">
|
||||
{/* TodoList */}
|
||||
<section className="border-b border-[var(--border)]">
|
||||
<div className="px-4 py-3 flex items-center gap-2 text-xs font-semibold text-[var(--text-muted)] uppercase">
|
||||
<ListTodo className="w-3.5 h-3.5" />
|
||||
任务列表
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
{tasksMd ? (
|
||||
<div className="text-sm prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-[var(--text-muted)]">暂无任务</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Modified Files */}
|
||||
<section>
|
||||
<div className="px-4 py-3 flex items-center gap-2 text-xs font-semibold text-[var(--text-muted)] uppercase">
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
修改的文件 ({files.length})
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
{files.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{files.map(f => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => openFile(f)}
|
||||
className="w-full flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-hover)] py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors text-left"
|
||||
>
|
||||
<FileText className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="truncate">{f}</span>
|
||||
<ExternalLink className="w-3 h-3 flex-shrink-0 ml-auto opacity-0 group-hover:opacity-100" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-[var(--text-muted)]">暂无修改的文件</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File preview modal */}
|
||||
{previewFile && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setPreviewFile(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-[var(--bg-secondary)] rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col shadow-2xl"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-[var(--accent)]" />
|
||||
<span className="font-semibold">{previewFile.name}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setPreviewFile(null)}
|
||||
className="w-8 h-8 rounded flex items-center justify-center text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-4 prose dark:prose-invert max-w-none text-sm">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import MDEditor from '@uiw/react-md-editor'
|
||||
import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus } from 'lucide-react'
|
||||
import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus, ExternalLink } from 'lucide-react'
|
||||
import { MarkdownEditor } from './MarkdownEditor'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
@ -170,18 +170,31 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
|
||||
<h2 className="font-semibold">{team.name}</h2>
|
||||
</div>
|
||||
{installed ? (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="flex items-center gap-1.5 text-[var(--text-muted)] hover:text-[var(--color-error)] text-sm bg-red-900/20 hover:bg-red-900/30 px-3 py-1.5 rounded-lg"
|
||||
>
|
||||
{deleting ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
{team.repo_url && (
|
||||
<a
|
||||
href={team.repo_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-[var(--text-muted)] hover:text-[var(--accent)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--bg-hover)]"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
查看 GitHub
|
||||
</a>
|
||||
)}
|
||||
卸载团队
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={deleting}
|
||||
className="flex items-center gap-1.5 text-[var(--text-muted)] hover:text-[var(--color-error)] text-sm bg-red-900/20 hover:bg-red-900/30 px-3 py-1.5 rounded-lg"
|
||||
>
|
||||
{deleting ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="w-4 h-4" />
|
||||
)}
|
||||
卸载团队
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
@ -204,20 +217,11 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
|
||||
{/* Team info */}
|
||||
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4">
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-2">{team.description}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-[var(--text-muted)]">
|
||||
{team.author && <span>作者: {team.author}</span>}
|
||||
{team.repo_url && (
|
||||
<a
|
||||
href={team.repo_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[var(--accent)] hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
项目页面
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{team.author && (
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
作者: {team.author}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Agents */}
|
||||
@ -270,18 +274,16 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
|
||||
))}
|
||||
</div>
|
||||
{/* Editor */}
|
||||
<div className="h-64 border-t border-[var(--border)]">
|
||||
<div className="border-t border-[var(--border)]">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-[var(--accent)]" />
|
||||
</div>
|
||||
) : (
|
||||
<MDEditor
|
||||
<MarkdownEditor
|
||||
value={agentContent}
|
||||
onChange={(v: string | undefined) => setAgentContent(v || '')}
|
||||
onChange={(v: string) => setAgentContent(v)}
|
||||
height={250}
|
||||
preview="edit"
|
||||
visibleDragbar={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -372,13 +374,11 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
|
||||
<div className="flex-1 flex flex-col border border-[var(--border)] rounded-lg overflow-hidden">
|
||||
{selectedKnowledge ? (
|
||||
<>
|
||||
<div className="h-80">
|
||||
<MDEditor
|
||||
<div className="flex-1">
|
||||
<MarkdownEditor
|
||||
value={knowledgeContent}
|
||||
onChange={(v: string | undefined) => setKnowledgeContent(v || '')}
|
||||
height={300}
|
||||
preview="edit"
|
||||
visibleDragbar={false}
|
||||
onChange={(v: string) => setKnowledgeContent(v)}
|
||||
height={320}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end px-4 py-2 bg-[var(--bg-tertiary)]">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import MDEditor from '@uiw/react-md-editor'
|
||||
import { User, Key, Save, Loader2 } from 'lucide-react'
|
||||
import { MarkdownEditor } from './MarkdownEditor'
|
||||
|
||||
const API = '/api'
|
||||
|
||||
@ -142,25 +142,10 @@ export function UserSettings() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<MDEditor
|
||||
<MarkdownEditor
|
||||
value={profile}
|
||||
onChange={(v) => setProfile(v || '')}
|
||||
onChange={(v: string) => setProfile(v)}
|
||||
height={300}
|
||||
preview="edit"
|
||||
visibleDragbar={false}
|
||||
className="border border-[var(--border)] rounded-lg overflow-hidden"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
}}
|
||||
textareaProps={{
|
||||
placeholder: '编辑个人资料介绍...',
|
||||
style: {
|
||||
fontFamily: 'var(--font-mono), monospace',
|
||||
fontSize: '13px',
|
||||
backgroundColor: 'var(--bg-primary)',
|
||||
color: 'var(--text-primary)',
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -75,145 +75,6 @@
|
||||
--channel-list-bg: #2B2D31;
|
||||
}
|
||||
|
||||
/* Custom styling for react-md-editor - Complete override */
|
||||
.w-md-editor {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 8px !important;
|
||||
background: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.w-md-editor * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.w-md-editor-toolbar {
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
background: var(--bg-secondary) !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.w-md-editor-toolbar button {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-toolbar button:hover {
|
||||
background: var(--bg-hover) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-toolbar button.active {
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-text-pre > code,
|
||||
.w-md-editor-text-input {
|
||||
font-family: var(--font-mono), monospace !important;
|
||||
font-size: 13px !important;
|
||||
line-height: 1.6 !important;
|
||||
color: var(--text-primary) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.w-md-editor-text-input {
|
||||
caret-color: var(--text-primary) !important;
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.w-md-editor-text-pre {
|
||||
padding: 12px !important;
|
||||
background: var(--bg-primary) !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview {
|
||||
padding: 12px !important;
|
||||
background: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview h1,
|
||||
.w-md-editor-preview h2,
|
||||
.w-md-editor-preview h3,
|
||||
.w-md-editor-preview h4,
|
||||
.w-md-editor-preview h5,
|
||||
.w-md-editor-preview h6 {
|
||||
color: var(--text-primary) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
padding-bottom: 0.3em !important;
|
||||
margin-top: 1.5em !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview a {
|
||||
color: var(--accent) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview code {
|
||||
background: var(--bg-tertiary) !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 4px !important;
|
||||
font-family: var(--font-mono), monospace !important;
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview pre {
|
||||
background: var(--bg-tertiary) !important;
|
||||
padding: 12px !important;
|
||||
border-radius: 6px !important;
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview pre code {
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview blockquote {
|
||||
border-left: 4px solid var(--accent) !important;
|
||||
padding-left: 16px !important;
|
||||
color: var(--text-secondary) !important;
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview ul,
|
||||
.w-md-editor-preview ol {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview table {
|
||||
border-collapse: collapse !important;
|
||||
width: 100% !important;
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview th,
|
||||
.w-md-editor-preview td {
|
||||
border: 1px solid var(--border) !important;
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
|
||||
.w-md-editor-preview th {
|
||||
background: var(--bg-tertiary) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-input {
|
||||
background: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.w-md-editor-text,
|
||||
.w-md-editor-text-pre {
|
||||
background: var(--bg-primary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
/* Hide drag bar in certain contexts */
|
||||
.w-md-editor .w-md-editor-bar {
|
||||
background: var(--bg-tertiary) !important;
|
||||
width: 4px !important;
|
||||
cursor: col-resize !important;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
|
||||
@ -29,6 +29,7 @@ interface AppState {
|
||||
fetchSkills: () => Promise<void>
|
||||
sendMessage: (roomId: string, content: string) => void
|
||||
connectRoom: (roomId: string) => void
|
||||
setRoomTeam: (roomId: string, team: string) => Promise<void>
|
||||
}
|
||||
|
||||
const API = '/api'
|
||||
@ -171,5 +172,18 @@ export const useStore = create<AppState>((set, get) => {
|
||||
set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } }))
|
||||
ws[roomId]?.send(JSON.stringify({ type: 'user_message', content, user_name: userName }))
|
||||
},
|
||||
|
||||
setRoomTeam: async (roomId, team) => {
|
||||
const res = await fetch(`${API}/rooms/${roomId}/team`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ team })
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
set(s => ({
|
||||
rooms: s.rooms.map(r => r.id === roomId ? { ...r, team, master: data.master } : r)
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -7,8 +7,11 @@ export interface Room {
|
||||
type: RoomType
|
||||
status: RoomStatus
|
||||
master: string
|
||||
members?: string[]
|
||||
activeAgent?: string
|
||||
action?: string
|
||||
color?: string
|
||||
team?: string
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user