Compare commits
9 Commits
pku
...
c00c0072b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c00c0072b8 | ||
|
|
6392301833 | ||
|
|
ab8c9e294d | ||
|
|
1aa9e280b0 | ||
|
|
041986f5cd | ||
|
|
00ef22505e | ||
|
|
b73419b7a0 | ||
|
|
974af053ca | ||
|
|
0c571dec21 |
24
backend/.claude/settings.local.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(//Users/zhaoweijie/Desktop/agent/AgentCoord/**)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(source:*)",
|
||||
"Bash(pip install:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(tree:*)",
|
||||
"Bash(export FAST_DESIGN_MODE=True:*)",
|
||||
"Bash(echo:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(lsof:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(xargs kill:*)",
|
||||
"Bash(pip:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:pypi.org)",
|
||||
"Bash(cp:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,30 @@
|
||||
import asyncio
|
||||
import openai
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
import yaml
|
||||
from termcolor import colored
|
||||
import os
|
||||
|
||||
# Helper function to avoid circular import
|
||||
def print_colored(text, text_color="green", background="on_white"):
|
||||
print(colored(text, text_color, background))
|
||||
|
||||
# load config (apikey, apibase, model)
|
||||
yaml_file = os.path.join(os.getcwd(), "config", "config.yaml")
|
||||
try:
|
||||
with open(yaml_file, "r", encoding="utf-8") as file:
|
||||
yaml_data = yaml.safe_load(file)
|
||||
except Exception:
|
||||
yaml_file = {}
|
||||
yaml_data = {}
|
||||
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE") or yaml_data.get(
|
||||
"OPENAI_API_BASE", "https://api.openai.com"
|
||||
)
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") or yaml_data.get(
|
||||
"OPENAI_API_KEY", ""
|
||||
)
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
|
||||
# Initialize OpenAI clients
|
||||
client = OpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
|
||||
async_client = AsyncOpenAI(api_key=OPENAI_API_KEY, base_url=OPENAI_API_BASE)
|
||||
MODEL: str = os.getenv("OPENAI_API_MODEL") or yaml_data.get(
|
||||
"OPENAI_API_MODEL", "gpt-4-turbo-preview"
|
||||
)
|
||||
@@ -71,14 +77,14 @@ def LLM_Completion(
|
||||
|
||||
async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
from groq import AsyncGroq
|
||||
client = AsyncGroq(api_key=GROQ_API_KEY)
|
||||
groq_client = AsyncGroq(api_key=GROQ_API_KEY)
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
print("Attempt to use Groq (Fase Design Mode):")
|
||||
try:
|
||||
stream = await client.chat.completions.create(
|
||||
response = await groq_client.chat.completions.create(
|
||||
messages=messages,
|
||||
# model='gemma-7b-it',
|
||||
model="mixtral-8x7b-32768",
|
||||
@@ -92,9 +98,9 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "failed"
|
||||
raise Exception("failed")
|
||||
|
||||
full_reply_content = stream.choices[0].message.content
|
||||
full_reply_content = response.choices[0].message.content
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
@@ -103,14 +109,14 @@ async def _achat_completion_stream_groq(messages: list[dict]) -> str:
|
||||
async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
|
||||
from mistralai.client import MistralClient
|
||||
from mistralai.models.chat_completion import ChatMessage
|
||||
client = MistralClient(api_key=MISTRAL_API_KEY)
|
||||
mistral_client = MistralClient(api_key=MISTRAL_API_KEY)
|
||||
# client=AsyncGroq(api_key=GROQ_API_KEY)
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
messages[len(messages) - 1]["role"] = "user"
|
||||
stream = client.chat(
|
||||
stream = mistral_client.chat(
|
||||
messages=[
|
||||
ChatMessage(
|
||||
role=message["role"], content=message["content"]
|
||||
@@ -119,14 +125,13 @@ async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
|
||||
],
|
||||
# model = "mistral-small-latest",
|
||||
model="open-mixtral-8x7b",
|
||||
# response_format={"type": "json_object"},
|
||||
)
|
||||
break # If the operation is successful, break the loop
|
||||
except Exception:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "failed"
|
||||
raise Exception("failed")
|
||||
|
||||
full_reply_content = stream.choices[0].message.content
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
@@ -135,15 +140,11 @@ async def _achat_completion_stream_mixtral(messages: list[dict]) -> str:
|
||||
|
||||
|
||||
async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
response = await openai.ChatCompletion.acreate(
|
||||
response = await async_client.chat.completions.create(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.3,
|
||||
timeout=3,
|
||||
timeout=30,
|
||||
model="gpt-3.5-turbo-16k",
|
||||
stream=True,
|
||||
)
|
||||
@@ -154,40 +155,33 @@ async def _achat_completion_stream_gpt35(messages: list[dict]) -> str:
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
choices = chunk["choices"]
|
||||
choices = chunk.choices
|
||||
if len(choices) > 0:
|
||||
chunk_message = chunk["choices"][0].get(
|
||||
"delta", {}
|
||||
) # extract the message
|
||||
chunk_message = chunk.choices[0].delta
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
if "content" in chunk_message:
|
||||
if chunk_message.content:
|
||||
print(
|
||||
colored(chunk_message["content"], "blue", "on_white"),
|
||||
colored(chunk_message.content, "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
|
||||
full_reply_content = "".join(
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
[m.content or "" for m in collected_messages if m is not None]
|
||||
)
|
||||
return full_reply_content
|
||||
|
||||
|
||||
async def _achat_completion_json(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
|
||||
max_attempts = 5
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
stream = await openai.ChatCompletion.acreate(
|
||||
response = await async_client.chat.completions.create(
|
||||
messages=messages,
|
||||
max_tokens=4096,
|
||||
n=1,
|
||||
stop=None,
|
||||
temperature=0.3,
|
||||
timeout=3,
|
||||
timeout=30,
|
||||
model=MODEL,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
@@ -196,60 +190,62 @@ async def _achat_completion_json(messages: list[dict]) -> str:
|
||||
if attempt < max_attempts - 1: # i is zero indexed
|
||||
continue
|
||||
else:
|
||||
raise "failed"
|
||||
raise Exception("failed")
|
||||
|
||||
full_reply_content = stream.choices[0].message.content
|
||||
full_reply_content = response.choices[0].message.content
|
||||
print(colored(full_reply_content, "blue", "on_white"), end="")
|
||||
print()
|
||||
return full_reply_content
|
||||
|
||||
|
||||
async def _achat_completion_stream(messages: list[dict]) -> str:
|
||||
openai.api_key = OPENAI_API_KEY
|
||||
openai.api_base = OPENAI_API_BASE
|
||||
response = await openai.ChatCompletion.acreate(
|
||||
**_cons_kwargs(messages), stream=True
|
||||
)
|
||||
try:
|
||||
response = await async_client.chat.completions.create(
|
||||
**_cons_kwargs(messages), stream=True
|
||||
)
|
||||
|
||||
# create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
collected_messages = []
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
choices = chunk["choices"]
|
||||
if len(choices) > 0:
|
||||
chunk_message = chunk["choices"][0].get(
|
||||
"delta", {}
|
||||
) # extract the message
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
if "content" in chunk_message:
|
||||
print(
|
||||
colored(chunk_message["content"], "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
# create variables to collect the stream of chunks
|
||||
collected_chunks = []
|
||||
collected_messages = []
|
||||
# iterate through the stream of events
|
||||
async for chunk in response:
|
||||
collected_chunks.append(chunk) # save the event response
|
||||
choices = chunk.choices
|
||||
if len(choices) > 0:
|
||||
chunk_message = chunk.choices[0].delta
|
||||
collected_messages.append(chunk_message) # save the message
|
||||
if chunk_message.content:
|
||||
print(
|
||||
colored(chunk_message.content, "blue", "on_white"),
|
||||
end="",
|
||||
)
|
||||
print()
|
||||
|
||||
full_reply_content = "".join(
|
||||
[m.get("content", "") for m in collected_messages]
|
||||
)
|
||||
return full_reply_content
|
||||
full_reply_content = "".join(
|
||||
[m.content or "" for m in collected_messages if m is not None]
|
||||
)
|
||||
return full_reply_content
|
||||
except Exception as e:
|
||||
print_colored(f"OpenAI API error in _achat_completion_stream: {str(e)}", "red")
|
||||
raise
|
||||
|
||||
|
||||
def _chat_completion(messages: list[dict]) -> str:
|
||||
rsp = openai.ChatCompletion.create(**_cons_kwargs(messages))
|
||||
content = rsp["choices"][0]["message"]["content"]
|
||||
return content
|
||||
try:
|
||||
rsp = client.chat.completions.create(**_cons_kwargs(messages))
|
||||
content = rsp.choices[0].message.content
|
||||
return content
|
||||
except Exception as e:
|
||||
print_colored(f"OpenAI API error in _chat_completion: {str(e)}", "red")
|
||||
raise
|
||||
|
||||
|
||||
def _cons_kwargs(messages: list[dict]) -> dict:
|
||||
kwargs = {
|
||||
"messages": messages,
|
||||
"max_tokens": 4096,
|
||||
"n": 1,
|
||||
"stop": None,
|
||||
"temperature": 0.5,
|
||||
"timeout": 3,
|
||||
"max_tokens": 2000,
|
||||
"temperature": 0.3,
|
||||
"timeout": 15,
|
||||
}
|
||||
kwargs_mode = {"model": MODEL}
|
||||
kwargs.update(kwargs_mode)
|
||||
|
||||
@@ -42,7 +42,7 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
|
||||
},
|
||||
]
|
||||
print(messages[1]["content"])
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
|
||||
PROMPT_AGENT_ABILITY_SCORING = """
|
||||
@@ -83,7 +83,6 @@ class JSON_Agent(BaseModel):
|
||||
class JSON_AGENT_ABILITY_SCORING(RootModel):
|
||||
root: Dict[str, JSON_Agent]
|
||||
|
||||
|
||||
def agentAbilityScoring(Agent_Board, Ability_Requirement_List):
|
||||
scoreTable = {}
|
||||
for Ability_Requirement in Ability_Requirement_List:
|
||||
@@ -105,7 +104,7 @@ def agentAbilityScoring(Agent_Board, Ability_Requirement_List):
|
||||
return scoreTable
|
||||
|
||||
|
||||
def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
async def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
Current_Task = {
|
||||
"TaskName": stepTask["StepName"],
|
||||
"InputObject_List": stepTask["InputObject_List"],
|
||||
@@ -125,10 +124,10 @@ def AgentSelectModify_init(stepTask, General_Goal, Agent_Board):
|
||||
),
|
||||
},
|
||||
]
|
||||
Ability_Requirement_List = read_LLM_Completion(messages)[
|
||||
Ability_Requirement_List = await read_LLM_Completion(messages)[
|
||||
"AbilityRequirement"
|
||||
]
|
||||
scoreTable = agentAbilityScoring(Agent_Board, Ability_Requirement_List)
|
||||
scoreTable = await agentAbilityScoring(Agent_Board, Ability_Requirement_List)
|
||||
return scoreTable
|
||||
|
||||
|
||||
|
||||
@@ -76,10 +76,13 @@ def generate_AbilityRequirement(General_Goal, Current_Task):
|
||||
},
|
||||
]
|
||||
print(messages[1]["content"])
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
return read_LLM_Completion(messages)["AbilityRequirement"]
|
||||
|
||||
def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
|
||||
# Check if Agent_Board is None or empty
|
||||
if Agent_Board is None or len(Agent_Board) == 0:
|
||||
raise ValueError("Agent_Board cannot be None or empty. Please ensure agents are set via /setAgents endpoint before generating a plan.")
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
@@ -99,7 +102,7 @@ def generate_AgentSelection(General_Goal, Current_Task, Agent_Board):
|
||||
agentboard_set = {agent["Name"] for agent in Agent_Board}
|
||||
|
||||
while True:
|
||||
candidate = read_LLM_Completion(messages)["AgentSelectionPlan"]
|
||||
candidate = read_LLM_Completion(messages)["AgentSelectionPlan"]
|
||||
if len(candidate) > MAX_TEAM_SIZE:
|
||||
teamSize = random.randint(2, MAX_TEAM_SIZE)
|
||||
candidate = candidate[0:teamSize]
|
||||
|
||||
@@ -9,6 +9,14 @@ import AgentCoord.util as util
|
||||
def generate_basePlan(
|
||||
General_Goal, Agent_Board, AgentProfile_Dict, InitialObject_List
|
||||
):
|
||||
Agent_Board = [
|
||||
{"Name": (a.get("Name") or "").strip(),"Profile": (a.get("Profile") or "").strip()}
|
||||
for a in Agent_Board
|
||||
if a and a.get("Name") is not None
|
||||
]
|
||||
if not Agent_Board: # 洗完后还是空 → 直接返回空计划
|
||||
return {"Plan_Outline": []}
|
||||
|
||||
basePlan = {
|
||||
"Initial Input Object": InitialObject_List,
|
||||
"Collaboration Process": [],
|
||||
|
||||
@@ -95,7 +95,7 @@ def branch_PlanOutline(
|
||||
},
|
||||
{"role": "system", "content": prompt},
|
||||
]
|
||||
Remaining_Steps = read_LLM_Completion(messages, useGroq=False)[
|
||||
Remaining_Steps = read_LLM_Completion(messages)[
|
||||
"Remaining Steps"
|
||||
]
|
||||
branch_List.append(Remaining_Steps)
|
||||
|
||||
@@ -145,7 +145,7 @@ def branch_TaskProcess(
|
||||
},
|
||||
{"role": "system", "content": prompt},
|
||||
]
|
||||
Remaining_Steps = read_LLM_Completion(messages, useGroq=False)[
|
||||
Remaining_Steps = read_LLM_Completion(messages)[
|
||||
"Remaining Steps"
|
||||
]
|
||||
|
||||
|
||||
@@ -83,4 +83,16 @@ def generate_PlanOutline(InitialObject_List, General_Goal):
|
||||
),
|
||||
},
|
||||
]
|
||||
return read_LLM_Completion(messages)["Plan_Outline"]
|
||||
result = read_LLM_Completion(messages)
|
||||
if isinstance(result, dict) and "Plan_Outline" in result:
|
||||
return result["Plan_Outline"]
|
||||
else:
|
||||
# 如果格式不正确,返回默认的计划大纲
|
||||
return [
|
||||
{
|
||||
"StepName": "Default Step",
|
||||
"TaskContent": "Generated default plan step due to format error",
|
||||
"InputObject_List": [],
|
||||
"OutputObject": "Default Output"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -106,6 +106,9 @@ class TaskProcessPlan(BaseModel):
|
||||
|
||||
|
||||
def generate_TaskProcess(General_Goal, Current_Task_Description):
|
||||
# 新增参数验证
|
||||
if not General_Goal or str(General_Goal).strip() == "":
|
||||
raise ValueError("General_Goal cannot be empty")
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
|
||||
@@ -80,10 +80,17 @@ class BaseAction():
|
||||
Important_Mark = ""
|
||||
action_Record += PROMPT_TEMPLATE_ACTION_RECORD.format(AgentName = actionInfo["AgentName"], Action_Description = actionInfo["AgentName"], Action_Result = actionInfo["Action_Result"], Important_Mark = Important_Mark)
|
||||
|
||||
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(agentName = agentName, agentProfile = AgentProfile_Dict[agentName], General_Goal = General_Goal, Current_Task_Description = TaskDescription, Input_Objects = inputObject_Record, History_Action = action_Record, Action_Description = self.info["Description"], Action_Custom_Note = self.Action_Custom_Note)
|
||||
# Handle missing agent profiles gracefully
|
||||
if agentName not in AgentProfile_Dict:
|
||||
print_colored(text=f"Warning: Agent '{agentName}' not found in AgentProfile_Dict. Using default profile.", text_color="yellow")
|
||||
agentProfile = f"AI Agent named {agentName}"
|
||||
else:
|
||||
agentProfile = AgentProfile_Dict[agentName]
|
||||
|
||||
prompt = PROMPT_TEMPLATE_TAKE_ACTION_BASE.format(agentName = agentName, agentProfile = agentProfile, General_Goal = General_Goal, Current_Task_Description = TaskDescription, Input_Objects = inputObject_Record, History_Action = action_Record, Action_Description = self.info["Description"], Action_Custom_Note = self.Action_Custom_Note)
|
||||
print_colored(text = prompt, text_color="red")
|
||||
messages = [{"role":"system", "content": prompt}]
|
||||
ActionResult = LLM_Completion(messages,True,False)
|
||||
ActionResult = LLM_Completion(messages,stream=False)
|
||||
ActionInfo_with_Result = copy.deepcopy(self.info)
|
||||
ActionInfo_with_Result["Action_Result"] = ActionResult
|
||||
|
||||
|
||||
@@ -85,9 +85,17 @@ def executePlan(plan, num_StepToRun, RehearsalLog, AgentProfile_Dict):
|
||||
# start the group chat
|
||||
util.print_colored(TaskDescription, text_color="green")
|
||||
ActionHistory = []
|
||||
action_count = 0
|
||||
total_actions = len(TaskProcess)
|
||||
|
||||
for ActionInfo in TaskProcess:
|
||||
action_count += 1
|
||||
actionType = ActionInfo["ActionType"]
|
||||
agentName = ActionInfo["AgentName"]
|
||||
|
||||
# 添加进度日志
|
||||
util.print_colored(f"🔄 Executing action {action_count}/{total_actions}: {actionType} by {agentName}", text_color="yellow")
|
||||
|
||||
if actionType in Action.customAction_Dict:
|
||||
currentAction = Action.customAction_Dict[actionType](
|
||||
info=ActionInfo,
|
||||
|
||||
@@ -1,2 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 导出常用模块和函数,根据实际需求调整
|
||||
from .PlanEngine.planOutline_Generator import generate_PlanOutline
|
||||
from .PlanEngine.basePlan_Generator import generate_basePlan
|
||||
from .PlanEngine.AgentSelection_Generator import generate_AgentSelection
|
||||
from .LLMAPI.LLMAPI import LLM_Completion
|
||||
from .util.converter import read_LLM_Completion
|
||||
|
||||
# 定义包元数据
|
||||
__version__ = "0.1.0"
|
||||
__all__ = [
|
||||
"generate_PlanOutline",
|
||||
"generate_basePlan",
|
||||
"generate_AgentSelection",
|
||||
"LLM_Completion",
|
||||
"read_LLM_Completion"
|
||||
]
|
||||
|
||||
@@ -20,6 +20,8 @@ def camel_case_to_normal(s):
|
||||
def generate_template_sentence_for_CollaborationBrief(
|
||||
input_object_list, output_object, agent_list, step_task
|
||||
):
|
||||
# Ensure step_task is not None
|
||||
step_task = step_task if step_task is not None else "perform the task"
|
||||
# Check if the names are in camel case (no spaces) and convert them to normal naming convention
|
||||
input_object_list = (
|
||||
[
|
||||
@@ -31,29 +33,48 @@ def generate_template_sentence_for_CollaborationBrief(
|
||||
)
|
||||
output_object = (
|
||||
camel_case_to_normal(output_object)
|
||||
if is_camel_case(output_object)
|
||||
else output_object
|
||||
if output_object is not None and is_camel_case(output_object)
|
||||
else (output_object if output_object is not None else "unknown output")
|
||||
)
|
||||
|
||||
# Format the agents into a string with proper grammar
|
||||
agent_str = (
|
||||
" and ".join([", ".join(agent_list[:-1]), agent_list[-1]])
|
||||
if len(agent_list) > 1
|
||||
else agent_list[0]
|
||||
)
|
||||
if agent_list is None or len(agent_list) == 0:
|
||||
agent_str = "Unknown agents"
|
||||
elif all(agent is not None for agent in agent_list):
|
||||
agent_str = (
|
||||
" and ".join([", ".join(agent_list[:-1]), agent_list[-1]])
|
||||
if len(agent_list) > 1
|
||||
else agent_list[0]
|
||||
)
|
||||
else:
|
||||
# Filter out None values
|
||||
filtered_agents = [agent for agent in agent_list if agent is not None]
|
||||
if filtered_agents:
|
||||
agent_str = (
|
||||
" and ".join([", ".join(filtered_agents[:-1]), filtered_agents[-1]])
|
||||
if len(filtered_agents) > 1
|
||||
else filtered_agents[0]
|
||||
)
|
||||
else:
|
||||
agent_str = "Unknown agents"
|
||||
|
||||
if input_object_list is None or len(input_object_list) == 0:
|
||||
# Combine all the parts into the template sentence
|
||||
template_sentence = f"{agent_str} perform the task of {step_task} to obtain {output_object}."
|
||||
else:
|
||||
# Format the input objects into a string with proper grammar
|
||||
input_str = (
|
||||
" and ".join(
|
||||
[", ".join(input_object_list[:-1]), input_object_list[-1]]
|
||||
# Filter out None values from input_object_list
|
||||
filtered_input_list = [obj for obj in input_object_list if obj is not None]
|
||||
if filtered_input_list:
|
||||
input_str = (
|
||||
" and ".join(
|
||||
[", ".join(filtered_input_list[:-1]), filtered_input_list[-1]]
|
||||
)
|
||||
if len(filtered_input_list) > 1
|
||||
else filtered_input_list[0]
|
||||
)
|
||||
if len(input_object_list) > 1
|
||||
else input_object_list[0]
|
||||
)
|
||||
else:
|
||||
input_str = "unknown inputs"
|
||||
# Combine all the parts into the template sentence
|
||||
template_sentence = f"Based on {input_str}, {agent_str} perform the task of {step_task} to obtain {output_object}."
|
||||
|
||||
@@ -90,7 +111,7 @@ def read_LLM_Completion(messages, useGroq=True):
|
||||
return json.loads(match.group(0).strip())
|
||||
except Exception:
|
||||
pass
|
||||
raise ("bad format!")
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
|
||||
|
||||
def read_json_content(text):
|
||||
@@ -111,7 +132,7 @@ def read_json_content(text):
|
||||
if match:
|
||||
return json.loads(match.group(0).strip())
|
||||
|
||||
raise ("bad format!")
|
||||
return {} # 返回空对象而不是抛出异常
|
||||
|
||||
|
||||
def read_outputObject_content(text, keyword):
|
||||
@@ -127,4 +148,4 @@ def read_outputObject_content(text, keyword):
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
raise ("bad format!")
|
||||
return "" # 返回空字符串而不是抛出异常
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
## config for default LLM
|
||||
OPENAI_API_BASE: ""
|
||||
OPENAI_API_KEY: ""
|
||||
OPENAI_API_MODEL: "gpt-4-turbo-preview"
|
||||
OPENAI_API_BASE: "https://ai.gitee.com/v1"
|
||||
OPENAI_API_KEY: "HYCNGM39GGFNSB1F8MBBMI9QYJR3P1CRSYS2PV1A"
|
||||
OPENAI_API_MODEL: "DeepSeek-V3"
|
||||
|
||||
## config for fast mode
|
||||
FAST_DESIGN_MODE: True
|
||||
GROQ_API_KEY: ""
|
||||
MISTRAL_API_KEY: ""
|
||||
|
||||
## options under experimentation, leave them as Fasle unless you know what it is for
|
||||
## options under experimentation, leave them as False unless you know what it is for
|
||||
USE_CACHE: False
|
||||
@@ -1,7 +1,7 @@
|
||||
Flask==3.0.2
|
||||
openai==0.28.1
|
||||
openai==2.8.1
|
||||
PyYAML==6.0.1
|
||||
termcolor==2.4.0
|
||||
groq==0.4.2
|
||||
mistralai==0.1.6
|
||||
mistralai==1.5.2
|
||||
socksio==1.0.0
|
||||
|
||||
13
backend/restart.ps1
Normal file
@@ -0,0 +1,13 @@
|
||||
# restart.ps1
|
||||
$port=8000
|
||||
|
||||
$env:PYTHONUNBUFFERED="1"
|
||||
python server.py --port 8000
|
||||
|
||||
|
||||
Write-Host "Killing PID on port $port..." -ForegroundColor Red
|
||||
Get-NetTCPConnection -LocalPort $port -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.OwningProcess -Force
|
||||
}
|
||||
Write-Host "Starting Flask..." -ForegroundColor Green
|
||||
python server.py --port $port
|
||||
@@ -213,12 +213,17 @@ def Handle_generate_basePlan():
|
||||
if requestIdentifier in Request_Cache:
|
||||
return jsonify(Request_Cache[requestIdentifier])
|
||||
|
||||
basePlan = generate_basePlan(
|
||||
General_Goal=incoming_data["General Goal"],
|
||||
Agent_Board=AgentBoard,
|
||||
AgentProfile_Dict=AgentProfile_Dict,
|
||||
InitialObject_List=incoming_data["Initial Input Object"],
|
||||
)
|
||||
try:
|
||||
basePlan = generate_basePlan(
|
||||
General_Goal=incoming_data["General Goal"],
|
||||
Agent_Board=AgentBoard,
|
||||
AgentProfile_Dict=AgentProfile_Dict,
|
||||
InitialObject_List=incoming_data["Initial Input Object"],
|
||||
)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"An unexpected error occurred: {str(e)}"}), 500
|
||||
basePlan_withRenderSpec = Add_Collaboration_Brief_FrontEnd(basePlan)
|
||||
Request_Cache[requestIdentifier] = basePlan_withRenderSpec
|
||||
response = jsonify(basePlan_withRenderSpec)
|
||||
@@ -278,10 +283,38 @@ def set_agents():
|
||||
|
||||
def init():
|
||||
global AgentBoard, AgentProfile_Dict, Request_Cache
|
||||
with open(
|
||||
os.path.join(os.getcwd(), "RequestCache", "Request_Cache.json"), "r"
|
||||
) as json_file:
|
||||
Request_Cache = json.load(json_file)
|
||||
|
||||
# Load Request Cache
|
||||
try:
|
||||
with open(
|
||||
os.path.join(os.getcwd(), "RequestCache", "Request_Cache.json"), "r"
|
||||
) as json_file:
|
||||
Request_Cache = json.load(json_file)
|
||||
print(f"✅ Loaded Request_Cache with {len(Request_Cache)} entries")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load Request_Cache: {e}")
|
||||
Request_Cache = {}
|
||||
|
||||
# Load Agent Board
|
||||
try:
|
||||
with open(
|
||||
os.path.join(os.getcwd(), "AgentRepo", "agentBoard_v1.json"), "r", encoding="utf-8"
|
||||
) as json_file:
|
||||
AgentBoard = json.load(json_file)
|
||||
print(f"✅ Loaded AgentBoard with {len(AgentBoard)} agents")
|
||||
|
||||
# Build AgentProfile_Dict
|
||||
AgentProfile_Dict = {}
|
||||
for item in AgentBoard:
|
||||
name = item["Name"]
|
||||
profile = item["Profile"]
|
||||
AgentProfile_Dict[name] = profile
|
||||
print(f"✅ Built AgentProfile_Dict with {len(AgentProfile_Dict)} profiles")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Failed to load AgentBoard: {e}")
|
||||
AgentBoard = []
|
||||
AgentProfile_Dict = {}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -291,8 +324,8 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8017,
|
||||
help="set the port number, 8017 by defaul.",
|
||||
default=8000,
|
||||
help="set the port number, 8000 by defaul.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
init()
|
||||
|
||||
159
backend/代码逻辑架构分析.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# AgentCoord Backend 代码逻辑架构分析
|
||||
|
||||
## 🏗️ Backend 代码逻辑架构
|
||||
|
||||
### 📁 核心目录结构
|
||||
```
|
||||
backend/
|
||||
├── server.py # Flask主服务器入口
|
||||
├── config/config.yaml # LLM API配置
|
||||
├── AgentRepo/agentBoard_v1.json # 智能体定义库
|
||||
├── DataProcess/ # 数据处理层
|
||||
├── RequestCache/ # 缓存机制
|
||||
└── AgentCoord/ # 核心业务逻辑
|
||||
├── LLMAPI/ # LLM接口封装
|
||||
├── PlanEngine/ # 计划生成引擎
|
||||
├── RehearsalEngine_V2/ # 计划执行引擎
|
||||
└── util/ # 工具模块
|
||||
```
|
||||
|
||||
### 🔄 主要工作流程
|
||||
|
||||
#### 1️⃣ **计划生成流程** (PlanEngine)
|
||||
```
|
||||
用户目标 → 生成计划大纲 → 选择智能体 → 生成任务流程 → 输出完整计划
|
||||
```
|
||||
|
||||
**核心模块:**
|
||||
- `basePlan_Generator.py` - 整合所有计划生成组件
|
||||
- `planOutline_Generator.py` - 生成高级计划大纲
|
||||
- `taskProcess_Generator.py` - 生成详细任务执行流程
|
||||
- `AgentSelection_Generator.py` - 选择最适合的智能体
|
||||
|
||||
#### 2️⃣ **计划执行流程** (RehearsalEngine_V2)
|
||||
```
|
||||
协作计划 → 初始化执行环境 → 按步骤执行 → 智能体协作 → 记录执行日志
|
||||
```
|
||||
|
||||
**动作类型:**
|
||||
- **Propose** - 提出建议和方案
|
||||
- **Critique** - 提供反馈和批评
|
||||
- **Improve** - 基于反馈改进结果
|
||||
- **Finalize** - 最终确定输出
|
||||
|
||||
#### 3️⃣ **LLM接口层** (LLMAPI)
|
||||
支持的模型:
|
||||
- OpenAI GPT-4/GPT-3.5
|
||||
- Groq Mixtral-8x7b (快速模式)
|
||||
- Mistral Open-Mixtral-8x7b
|
||||
|
||||
### 🌐 API端点架构
|
||||
|
||||
#### **计划生成APIs**
|
||||
- `POST /generate_basePlan` - 生成基础协作计划
|
||||
- `POST /fill_stepTask` - 填充步骤任务详情
|
||||
- `POST /branch_PlanOutline` - 处理计划分支
|
||||
|
||||
#### **计划执行APIs**
|
||||
- `POST /executePlan` - 执行协作计划
|
||||
- `POST /agentSelectModify_init` - 初始化智能体选择
|
||||
|
||||
#### **系统管理APIs**
|
||||
- `POST /setAgents` - 设置智能体板
|
||||
- `POST /_saveRequestCache` - 保存请求缓存
|
||||
|
||||
### 💾 数据流设计
|
||||
|
||||
#### **输入层**
|
||||
- HTTP请求验证
|
||||
- 参数提取和格式化
|
||||
- 缓存检查
|
||||
|
||||
#### **业务逻辑层**
|
||||
- LLM API调用
|
||||
- 多智能体协调
|
||||
- 任务状态管理
|
||||
|
||||
#### **输出层**
|
||||
- 结果格式化
|
||||
- 前端渲染模板生成
|
||||
- JSON响应
|
||||
|
||||
### ⚙️ 配置与优化
|
||||
|
||||
#### **配置文件 (config.yaml)**
|
||||
```yaml
|
||||
OPENAI_API_BASE: "https://api.openai.com"
|
||||
OPENAI_API_KEY: "your-key"
|
||||
OPENAI_API_MODEL: "gpt-4-turbo-preview"
|
||||
FAST_DESIGN_MODE: True # 启用快速模式
|
||||
USE_CACHE: False # 缓存开关
|
||||
```
|
||||
|
||||
#### **性能优化**
|
||||
- 请求缓存机制
|
||||
- 快速模式支持(Groq)
|
||||
- 异步LLM调用
|
||||
- 重试和错误处理
|
||||
|
||||
### 🔧 关键技术特点
|
||||
|
||||
1. **模块化架构** - 清晰的职责分离
|
||||
2. **多LLM支持** - 灵活的模型切换
|
||||
3. **智能体协作** - 复杂的多智能体工作流
|
||||
4. **前端适配** - 自动生成渲染模板
|
||||
5. **可扩展性** - 支持自定义智能体和动作
|
||||
|
||||
## 📋 详细模块说明
|
||||
|
||||
### 1. 服务器入口 (server.py)
|
||||
- **功能**: Flask应用主入口,提供RESTful API
|
||||
- **特点**: 支持请求缓存、全局状态管理、参数化端口配置
|
||||
|
||||
### 2. 计划引擎 (PlanEngine)
|
||||
**核心功能**: 生成多智能体协作计划
|
||||
|
||||
- **basePlan_Generator.py**: 整合所有生成器,生成完整协作计划
|
||||
- **planOutline_Generator.py**: 基于目标生成计划大纲
|
||||
- **taskProcess_Generator.py**: 为每个任务步骤生成执行流程
|
||||
- **AgentSelection_Generator.py**: 选择合适的智能体执行任务
|
||||
|
||||
### 3. 排练引擎 (RehearsalEngine_V2)
|
||||
**核心功能**: 执行生成的协作计划
|
||||
|
||||
- **ExecutePlan.py**: 计划执行控制器
|
||||
- **Action模块**: 实现各种协作动作(Propose, Critique, Improve, Finalize)
|
||||
|
||||
### 4. LLM API接口 (LLMAPI)
|
||||
**核心功能**: 封装多种大语言模型API
|
||||
- 支持流式响应
|
||||
- 异步处理
|
||||
- 快速模式切换
|
||||
|
||||
### 5. 数据处理 (DataProcess)
|
||||
**核心功能**: 格式转换和前端适配
|
||||
- 颜色映射:不同元素类型的视觉区分
|
||||
- 模板生成:为前端生成渲染模板
|
||||
- 格式化:处理驼峰命名和自然语言转换
|
||||
|
||||
## 🚀 启动和调试
|
||||
|
||||
### 开发环境启动
|
||||
```bash
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
python server.py --port 8017
|
||||
```
|
||||
|
||||
### 调试模式
|
||||
Flask已内置debug=True,支持:
|
||||
- 交互式调试器
|
||||
- 自动重载
|
||||
- 详细错误页面
|
||||
|
||||
### Docker部署
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
这个backend实现了一个完整的多智能体协作平台,通过精心设计的模块化架构,支持复杂任务的规划和执行。
|
||||
@@ -17,7 +17,7 @@ import _ from 'lodash';
|
||||
// fakeAgentSelections,
|
||||
// fakeCurrentAgentSelection,
|
||||
// } from './data/fakeAgentAssignment';
|
||||
import CheckIcon from '@/icons/CheckIcon';
|
||||
import CheckIcon from '@/icons/checkIcon';
|
||||
import AgentIcon from '@/components/AgentIcon';
|
||||
import { globalStorage } from '@/storage';
|
||||
import SendIcon from '@/icons/SendIcon';
|
||||
|
||||
9
frontend/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npm run type-check:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
6
frontend/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.idea
|
||||
.vscode
|
||||
.git
|
||||
.gitignore
|
||||
8
frontend/.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
API_BASE=http://127.0.0.1:8000
|
||||
79
frontend/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"ShallowRef": true,
|
||||
"Slot": true,
|
||||
"Slots": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"getCurrentWatcher": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"isShallow": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useId": true,
|
||||
"useModel": true,
|
||||
"useSlots": true,
|
||||
"useTemplateRef": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
1
frontend/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
6
frontend/.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
9
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
95
frontend/CLAUDE.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Development server with hot reload
|
||||
pnpm dev
|
||||
|
||||
# Build for production
|
||||
pnpm build
|
||||
|
||||
# Type checking
|
||||
pnpm type-check
|
||||
|
||||
# Lint and fix code
|
||||
pnpm lint
|
||||
|
||||
# Format code
|
||||
pnpm format
|
||||
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
This is a **Multi-Agent Coordination Platform** (多智能体协同平台) built with Vue 3, TypeScript, and Vite. The application enables users to create and manage AI agents with specialized roles and coordinate them to complete complex tasks through visual workflows.
|
||||
|
||||
### Tech Stack
|
||||
- **Vue 3** with Composition API and TypeScript
|
||||
- **Vite** for build tooling and development
|
||||
- **Element Plus** for UI components
|
||||
- **Pinia** for state management
|
||||
- **Tailwind CSS** for styling
|
||||
- **Vue Router** for routing (minimal usage)
|
||||
- **JSPlumb** for visual workflow connections
|
||||
- **Axios** for API requests with custom interceptors
|
||||
|
||||
### Key Architecture Components
|
||||
|
||||
#### State Management (`src/stores/modules/agents.ts`)
|
||||
Central store managing:
|
||||
- Agent definitions with profiles and icons
|
||||
- Task workflow data structures (`IRawStepTask`, `TaskProcess`)
|
||||
- Search functionality and current task state
|
||||
- Raw plan responses with UUID generation for tasks
|
||||
|
||||
#### Request Layer (`src/utils/request.ts`)
|
||||
Custom Axios wrapper with:
|
||||
- Proxy configuration for `/api` -> `http://localhost:8000`
|
||||
- Response interceptors for error handling
|
||||
- `useRequest` hook for reactive data fetching
|
||||
- Integrated Element Plus notifications
|
||||
|
||||
#### Component Structure
|
||||
- **Layout System** (`src/layout/`): Main application layout with Header and Main sections
|
||||
- **Task Templates** (`src/layout/components/Main/TaskTemplate/`): Different task types including AgentRepo, TaskSyllabus, and TaskResult
|
||||
- **Visual Workflow**: JSPlumb integration for drag-and-drop agent coordination flows
|
||||
|
||||
#### Icon System
|
||||
- SVG icons stored in `src/assets/icons/`
|
||||
- Custom `SvgIcon` component with vite-plugin-svg-icons
|
||||
- Icon categories include specialist roles (doctor, engineer, researcher, etc.)
|
||||
|
||||
### Build Configuration
|
||||
|
||||
#### Vite (`vite.config.ts`)
|
||||
- Element Plus auto-import and component resolution
|
||||
- SVG icon caching with custom symbol IDs
|
||||
- Proxy setup for API requests to backend
|
||||
- Path aliases: `@/` maps to `src/`
|
||||
|
||||
#### Docker Deployment
|
||||
- Multi-stage build: Node.js build + Caddy web server
|
||||
- API proxy configured via Caddyfile
|
||||
- Environment variable support for different deployment modes
|
||||
|
||||
### Data Models
|
||||
Key interfaces for the agent coordination system:
|
||||
- `Agent`: Name, Profile, Icon
|
||||
- `IRawStepTask`: Individual task steps with agent selection and inputs
|
||||
- `TaskProcess`: Action descriptions with important inputs
|
||||
- `IRichText`: Template-based content formatting with style support
|
||||
|
||||
### Development Notes
|
||||
- Uses pnpm as package manager (required by package.json)
|
||||
- Node version constraint: ^20.19.0 or >=22.12.0
|
||||
- Dark theme enabled by default in App.vue
|
||||
- Auto-imports configured for Vue APIs
|
||||
- No traditional Vue routes - uses component-based navigation
|
||||
27
frontend/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
ARG CADDY_VERSION=2.6
|
||||
ARG BUILD_ENV=prod
|
||||
|
||||
FROM node:20.19.0 as base
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm install
|
||||
RUN pnpm build
|
||||
|
||||
|
||||
# The base for mode ENVIRONMENT=prod
|
||||
FROM caddy:${CADDY_VERSION}-alpine as prod
|
||||
|
||||
# Workaround for https://github.com/alpinelinux/docker-alpine/issues/98#issuecomment-679278499
|
||||
RUN sed -i 's/https/http/' /etc/apk/repositories \
|
||||
&& apk add --no-cache bash
|
||||
|
||||
COPY docker/Caddyfile /etc/caddy/
|
||||
COPY --from=base /app/dist /frontend
|
||||
|
||||
# Run stage
|
||||
FROM ${BUILD_ENV}
|
||||
|
||||
EXPOSE 80 443
|
||||
VOLUME ["/data", "/etc/caddy"]
|
||||
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
|
||||
106
frontend/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# 多智能体协同平台 (Agent Coordination Platform)
|
||||
|
||||
一个强大的可视化平台,用于创建和管理具有专门角色的AI智能体,通过直观的工作流程协调它们来完成复杂任务。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **多智能体系统**:创建具有专门角色和专业知识的AI智能体
|
||||
- **可视化工作流编辑器**:使用JSPlumb设计智能体协调流程的拖放界面
|
||||
- **任务管理**:定义、执行和跟踪复杂的多步骤任务
|
||||
- **实时通信**:无缝的智能体交互和协调
|
||||
- **丰富的模板系统**:支持样式的灵活内容格式化
|
||||
- **TypeScript支持**:整个应用程序的完整类型安全
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 开发命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 开发服务器(热重载)
|
||||
pnpm dev
|
||||
|
||||
# 生产构建
|
||||
pnpm build
|
||||
|
||||
# 类型检查
|
||||
pnpm type-check
|
||||
|
||||
# 代码检查和修复
|
||||
pnpm lint
|
||||
|
||||
# 代码格式化
|
||||
pnpm format
|
||||
|
||||
# 运行单元测试
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Node.js ^20.19.0 或 >=22.12.0
|
||||
- pnpm(必需的包管理器)
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **Vue 3**:Composition API 和 TypeScript
|
||||
- **Vite**:构建工具和开发环境
|
||||
- **Element Plus**:UI组件库
|
||||
- **Pinia**:状态管理
|
||||
- **Tailwind CSS**:样式框架
|
||||
- **JSPlumb**:可视化工作流连接
|
||||
- **Axios**:API请求与自定义拦截器
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 状态管理
|
||||
中央存储管理智能体定义、任务工作流和协调状态
|
||||
|
||||
#### 请求层
|
||||
自定义Axios包装器,具有代理配置和集成通知
|
||||
|
||||
#### 可视化工作流
|
||||
JSPlumb集成,用于拖放智能体协调流程
|
||||
|
||||
#### 图标系统
|
||||
基于SVG的图标,用于不同的智能体专业化和角色
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── assets/ # 静态资源,包括智能体图标
|
||||
├── components/ # 可复用的Vue组件
|
||||
├── layout/ # 应用布局和主要组件
|
||||
├── stores/ # Pinia状态管理
|
||||
├── utils/ # 工具函数和请求层
|
||||
├── views/ # 页面组件
|
||||
└── App.vue # 根组件
|
||||
```
|
||||
|
||||
## 🎯 开发指南
|
||||
|
||||
### IDE设置
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar)(禁用Vetur)。
|
||||
|
||||
### 浏览器开发工具
|
||||
|
||||
- 基于Chromium的浏览器:
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- 在DevTools中启用自定义对象格式化程序
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- 在DevTools中启用自定义对象格式化程序
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
应用程序支持Docker部署,使用多阶段构建过程:Node.js用于构建,Caddy作为Web服务器。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT许可证 - 详见LICENSE文件
|
||||
75
frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue').EffectScope
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const ElNotification: typeof import('element-plus/es').ElNotification
|
||||
const computed: typeof import('vue').computed
|
||||
const createApp: typeof import('vue').createApp
|
||||
const customRef: typeof import('vue').customRef
|
||||
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||
const defineComponent: typeof import('vue').defineComponent
|
||||
const effectScope: typeof import('vue').effectScope
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
const h: typeof import('vue').h
|
||||
const inject: typeof import('vue').inject
|
||||
const isProxy: typeof import('vue').isProxy
|
||||
const isReactive: typeof import('vue').isReactive
|
||||
const isReadonly: typeof import('vue').isReadonly
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const markRaw: typeof import('vue').markRaw
|
||||
const nextTick: typeof import('vue').nextTick
|
||||
const onActivated: typeof import('vue').onActivated
|
||||
const onBeforeMount: typeof import('vue').onBeforeMount
|
||||
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||
const onDeactivated: typeof import('vue').onDeactivated
|
||||
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||
const onMounted: typeof import('vue').onMounted
|
||||
const onRenderTracked: typeof import('vue').onRenderTracked
|
||||
const onRenderTriggered: typeof import('vue').onRenderTriggered
|
||||
const onScopeDispose: typeof import('vue').onScopeDispose
|
||||
const onServerPrefetch: typeof import('vue').onServerPrefetch
|
||||
const onUnmounted: typeof import('vue').onUnmounted
|
||||
const onUpdated: typeof import('vue').onUpdated
|
||||
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||
const provide: typeof import('vue').provide
|
||||
const reactive: typeof import('vue').reactive
|
||||
const readonly: typeof import('vue').readonly
|
||||
const ref: typeof import('vue').ref
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const shallowReactive: typeof import('vue').shallowReactive
|
||||
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||
const shallowRef: typeof import('vue').shallowRef
|
||||
const toRaw: typeof import('vue').toRaw
|
||||
const toRef: typeof import('vue').toRef
|
||||
const toRefs: typeof import('vue').toRefs
|
||||
const toValue: typeof import('vue').toValue
|
||||
const triggerRef: typeof import('vue').triggerRef
|
||||
const unref: typeof import('vue').unref
|
||||
const useAttrs: typeof import('vue').useAttrs
|
||||
const useCssModule: typeof import('vue').useCssModule
|
||||
const useCssVars: typeof import('vue').useCssVars
|
||||
const useId: typeof import('vue').useId
|
||||
const useModel: typeof import('vue').useModel
|
||||
const useSlots: typeof import('vue').useSlots
|
||||
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||
const watch: typeof import('vue').watch
|
||||
const watchEffect: typeof import('vue').watchEffect
|
||||
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
205
frontend/claude_code_env.sh
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ========================
|
||||
# 常量定义
|
||||
# ========================
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
NODE_MIN_VERSION=18
|
||||
NODE_INSTALL_VERSION=22
|
||||
NVM_VERSION="v0.40.3"
|
||||
CLAUDE_PACKAGE="@anthropic-ai/claude-code"
|
||||
CONFIG_DIR="$HOME/.claude"
|
||||
CONFIG_FILE="$CONFIG_DIR/settings.json"
|
||||
API_BASE_URL="https://open.bigmodel.cn/api/anthropic"
|
||||
API_KEY_URL="https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys"
|
||||
API_TIMEOUT_MS=3000000
|
||||
|
||||
# ========================
|
||||
# 工具函数
|
||||
# ========================
|
||||
|
||||
log_info() {
|
||||
echo "🔹 $*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo "✅ $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "❌ $*" >&2
|
||||
}
|
||||
|
||||
ensure_dir_exists() {
|
||||
local dir="$1"
|
||||
if [ ! -d "$dir" ]; then
|
||||
mkdir -p "$dir" || {
|
||||
log_error "Failed to create directory: $dir"
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Node.js 安装函数
|
||||
# ========================
|
||||
|
||||
install_nodejs() {
|
||||
local platform=$(uname -s)
|
||||
|
||||
case "$platform" in
|
||||
Linux|Darwin)
|
||||
log_info "Installing Node.js on $platform..."
|
||||
|
||||
# 安装 nvm
|
||||
log_info "Installing nvm ($NVM_VERSION)..."
|
||||
curl -s https://raw.githubusercontent.com/nvm-sh/nvm/"$NVM_VERSION"/install.sh | bash
|
||||
|
||||
# 加载 nvm
|
||||
log_info "Loading nvm environment..."
|
||||
\. "$HOME/.nvm/nvm.sh"
|
||||
|
||||
# 安装 Node.js
|
||||
log_info "Installing Node.js $NODE_INSTALL_VERSION..."
|
||||
nvm install "$NODE_INSTALL_VERSION"
|
||||
|
||||
# 验证安装
|
||||
node -v &>/dev/null || {
|
||||
log_error "Node.js installation failed"
|
||||
exit 1
|
||||
}
|
||||
log_success "Node.js installed: $(node -v)"
|
||||
log_success "npm version: $(npm -v)"
|
||||
;;
|
||||
*)
|
||||
log_error "Unsupported platform: $platform"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Node.js 检查函数
|
||||
# ========================
|
||||
|
||||
check_nodejs() {
|
||||
if command -v node &>/dev/null; then
|
||||
current_version=$(node -v | sed 's/v//')
|
||||
major_version=$(echo "$current_version" | cut -d. -f1)
|
||||
|
||||
if [ "$major_version" -ge "$NODE_MIN_VERSION" ]; then
|
||||
log_success "Node.js is already installed: v$current_version"
|
||||
return 0
|
||||
else
|
||||
log_info "Node.js v$current_version is installed but version < $NODE_MIN_VERSION. Upgrading..."
|
||||
install_nodejs
|
||||
fi
|
||||
else
|
||||
log_info "Node.js not found. Installing..."
|
||||
install_nodejs
|
||||
fi
|
||||
}
|
||||
|
||||
# ========================
|
||||
# Claude Code 安装
|
||||
# ========================
|
||||
|
||||
install_claude_code() {
|
||||
if command -v claude &>/dev/null; then
|
||||
log_success "Claude Code is already installed: $(claude --version)"
|
||||
else
|
||||
log_info "Installing Claude Code..."
|
||||
npm install -g "$CLAUDE_PACKAGE" || {
|
||||
log_error "Failed to install claude-code"
|
||||
exit 1
|
||||
}
|
||||
log_success "Claude Code installed successfully"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_claude_json(){
|
||||
node --eval '
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const filePath = path.join(homeDir, ".claude.json");
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
||||
fs.writeFileSync(filePath, JSON.stringify({ ...content, hasCompletedOnboarding: true }, null, 2), "utf-8");
|
||||
} else {
|
||||
fs.writeFileSync(filePath, JSON.stringify({ hasCompletedOnboarding: true }, null, 2), "utf-8");
|
||||
}'
|
||||
}
|
||||
|
||||
# ========================
|
||||
# API Key 配置
|
||||
# ========================
|
||||
|
||||
configure_claude() {
|
||||
log_info "Configuring Claude Code..."
|
||||
echo " You can get your API key from: $API_KEY_URL"
|
||||
read -s -p "🔑 Please enter your ZHIPU API key: " api_key
|
||||
echo
|
||||
|
||||
if [ -z "$api_key" ]; then
|
||||
log_error "API key cannot be empty. Please run the script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_dir_exists "$CONFIG_DIR"
|
||||
|
||||
# 写入配置文件
|
||||
node --eval '
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const homeDir = os.homedir();
|
||||
const filePath = path.join(homeDir, ".claude", "settings.json");
|
||||
const apiKey = "'"$api_key"'";
|
||||
|
||||
const content = fs.existsSync(filePath)
|
||||
? JSON.parse(fs.readFileSync(filePath, "utf-8"))
|
||||
: {};
|
||||
|
||||
fs.writeFileSync(filePath, JSON.stringify({
|
||||
...content,
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: apiKey,
|
||||
ANTHROPIC_BASE_URL: "'"$API_BASE_URL"'",
|
||||
API_TIMEOUT_MS: "'"$API_TIMEOUT_MS"'",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1
|
||||
}
|
||||
}, null, 2), "utf-8");
|
||||
' || {
|
||||
log_error "Failed to write settings.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_success "Claude Code configured successfully"
|
||||
}
|
||||
|
||||
# ========================
|
||||
# 主流程
|
||||
# ========================
|
||||
|
||||
main() {
|
||||
echo "🚀 Starting $SCRIPT_NAME"
|
||||
|
||||
check_nodejs
|
||||
install_claude_code
|
||||
configure_claude_json
|
||||
configure_claude
|
||||
|
||||
echo ""
|
||||
log_success "🎉 Installation completed successfully!"
|
||||
echo ""
|
||||
echo "🚀 You can now start using Claude Code with:"
|
||||
echo " claude"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
35
frontend/components.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
MultiLineTooltip: typeof import('./src/components/MultiLineTooltip/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
}
|
||||
}
|
||||
BIN
frontend/dist.zip
Normal file
14
frontend/docker/Caddyfile
Normal file
@@ -0,0 +1,14 @@
|
||||
:80
|
||||
|
||||
# Proxy `/api` to backends
|
||||
handle_path /api/* {
|
||||
reverse_proxy {$API_HOST}
|
||||
}
|
||||
|
||||
# Frontend
|
||||
handle {
|
||||
root * /frontend
|
||||
encode gzip
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
}
|
||||
15
frontend/docker/docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
agent-coord-font:
|
||||
image: agent-coord:0.0.1
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
environment:
|
||||
- API_HOST="http://host.docker.internal:8000"
|
||||
- BUILD_ENV=prod
|
||||
4
frontend/env.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare global {
|
||||
const testGlobal: any; // 声明 testGlobal 为全局变量
|
||||
}
|
||||
48
frontend/eslint.config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginVitest from '@vitest/eslint-plugin'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
import autoImportConfig from './.eslintrc-auto-import.json' with { type: 'json' }
|
||||
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
},
|
||||
skipFormatting,
|
||||
{
|
||||
name: 'app/custom-rules',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'auto-import-globals',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...(autoImportConfig.globals || {}),
|
||||
testGlobal: 'readonly' // 手动添加一个测试变量
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off', // 确保关闭 no-undef 规则
|
||||
'@typescript-eslint/no-undef': 'off'
|
||||
}
|
||||
}
|
||||
)
|
||||
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/logo.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>多智能体协同平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
11986
frontend/package-lock.json
generated
Normal file
66
frontend/package.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "agent-coord",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix --cache",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jsplumb/browser-ui": "^6.2.10",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@vueuse/core": "^14.0.0",
|
||||
"axios": "^1.12.2",
|
||||
"dompurify": "^3.3.0",
|
||||
"element-plus": "^2.11.5",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"pinia": "^3.0.3",
|
||||
"qs": "^6.14.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/jsdom": "^27.0.0",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/node": "^22.18.11",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/eslint-plugin": "^1.3.23",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-vue": "~10.5.0",
|
||||
"jiti": "^2.6.1",
|
||||
"jsdom": "^27.0.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"typescript": "~5.9.0",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.1.1"
|
||||
}
|
||||
}
|
||||
6750
frontend/pnpm-lock.yaml
generated
Normal file
116
frontend/public/agent.json
Normal file
@@ -0,0 +1,116 @@
|
||||
[
|
||||
{
|
||||
"Icon": "Hailey_Johnson.png",
|
||||
"Name": "船舶设计师",
|
||||
"Profile": "提供船舶制造中的实际需求和约束。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Jennifer_Moore.png",
|
||||
"Name": "防护工程专家",
|
||||
"Profile": "专注于船舶腐蚀防护技术的设计与应用。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Jane_Moreno.png",
|
||||
"Name": "病理生理学家",
|
||||
"Profile": "专注于失血性休克的疾病机制,为药物研发提供理论靶点。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Giorgio_Rossi.png",
|
||||
"Name": "药物化学家",
|
||||
"Profile": "负责将靶点概念转化为实际可合成的分子。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Tamara_Taylor.png",
|
||||
"Name": "制剂工程师",
|
||||
"Profile": "负责将活性药物成分(API)变成稳定、可用、符合战场要求的剂型。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Maria_Lopez.png",
|
||||
"Name": "监管事务专家",
|
||||
"Profile": "深谙药品审评法规,目标是找到最快的合法上市路径。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Sam_Moore.png",
|
||||
"Name": "物理学家",
|
||||
"Profile": "从热力学与统计力学的基本原理出发,研究液态金属的自由能、焓、熵、比热等参数的理论建模。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Yuriko_Yamamoto.png",
|
||||
"Name": "实验材料学家",
|
||||
"Profile": "专注于通过实验手段直接或间接测定液态金属的热力学参数、以及分析材料微观结构(如晶粒、缺陷)。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Carlos_Gomez.png",
|
||||
"Name": "计算模拟专家",
|
||||
"Profile": "侧重于利用数值计算和模拟技术获取液态金属的热力学参数。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "John_Lin.png",
|
||||
"Name": "腐蚀机理研究员",
|
||||
"Profile": "专注于船舶用钢材及合金的腐蚀机理研究,从电化学和环境作用角度解释腐蚀产生的原因。在你的总结回答中,必须引用来自数联网的搜索数据,是搜索数据,不是数联网的研究成果。",
|
||||
"Classification": "船舶制造数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Arthur_Burton.png",
|
||||
"Name": "先进材料研发员",
|
||||
"Profile": "专注于开发和评估新型耐腐蚀材料、复合材料及固态电池材料。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Eddy_Lin.png",
|
||||
"Name": "肾脏病学家",
|
||||
"Profile": "专注于慢性肾脏病的诊断、治疗和患者管理,能提供临床洞察。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Isabella_Rodriguez.png",
|
||||
"Name": "临床研究协调员",
|
||||
"Profile": "负责受试者招募和临床试验流程优化。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Latoya_Williams.png",
|
||||
"Name": "中医药专家",
|
||||
"Profile": "理解药物的中药成分和作用机制。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Carmen_Ortiz.png",
|
||||
"Name": "药物安全专家",
|
||||
"Profile": "专注于药物不良反应数据收集、分析和报告。",
|
||||
"Classification": "医药数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Rajiv_Patel.png",
|
||||
"Name": "二维材料科学家",
|
||||
"Profile": "专注于二维材料(如石墨烯)的合成、性质和应用。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Tom_Moreno.png",
|
||||
"Name": "光电物理学家",
|
||||
"Profile": "研究材料的光电转换机制和关键影响因素。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Ayesha_Khan.png",
|
||||
"Name": "机器学习专家",
|
||||
"Profile": "专注于开发和应用AI模型用于材料模拟。",
|
||||
"Classification": "科学数据空间"
|
||||
},
|
||||
{
|
||||
"Icon": "Mei_Lin.png",
|
||||
"Name": "流体动力学专家",
|
||||
"Profile": "专注于流体行为理论和模拟。",
|
||||
"Classification": "科学数据空间"
|
||||
}
|
||||
]
|
||||
19
frontend/public/config.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "数联网",
|
||||
"subTitle": "众创智能体",
|
||||
"centerTitle": "多智能体协同平台",
|
||||
"taskPromptWords": [
|
||||
"如何快速筛选慢性肾脏病药物潜在受试者?",
|
||||
"如何补充“丹芍活血胶囊”不良反应数据?",
|
||||
"如何快速研发用于战场失血性休克的药物?",
|
||||
"二维材料的光电性质受哪些关键因素影响?",
|
||||
"如何通过AI模拟的方法分析材料的微观结构?",
|
||||
"如何分析获取液态金属热力学参数?",
|
||||
"如何解决固态电池的成本和寿命难题?",
|
||||
"如何解决船舶制造中的材料腐蚀难题?",
|
||||
"如何解决船舶制造中流体模拟和建模优化难题?"
|
||||
],
|
||||
"agentRepository": {
|
||||
"storageVersionIdentifier": "1"
|
||||
}
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
22
frontend/public/iodConfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "3D Semantic-Geometric Corrosion Mapping Implementations",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/d8f3ff8c-3fb3-4573-88a6-5dd823627c37",
|
||||
"fromRepo": "https://arxiv.org/abs/2404.13691"
|
||||
},
|
||||
{
|
||||
"name": "RustSEG -- Automated segmentation of corrosion using deep learning",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/67445299-110a-4a4e-9fda-42e4b5a493c2",
|
||||
"fromRepo": "https://arxiv.org/abs/2205.05426"
|
||||
},
|
||||
{
|
||||
"name": "Pixel-level Corrosion Detection on Metal Constructions by Fusion of Deep Learning Semantic and Contour Segmentation",
|
||||
"data_space": "江苏省产研院",
|
||||
"doId": "bdware.scenario/115d5135-85d3-4123-8b81-9eb9f07b6153",
|
||||
"fromRepo": "https://arxiv.org/abs/2008.05204"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
frontend/public/logo.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
97
frontend/src/App.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import Layout from './layout/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<layout />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#app {
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.jtk-endpoint {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-tertiary);
|
||||
border: 2px solid var(--color-card-border);
|
||||
box-shadow: var(--color-card-border-hover);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-content-hover);
|
||||
box-shadow: none;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--gradient: linear-gradient(to right, #0093eb, #00d2d1);
|
||||
}
|
||||
|
||||
#task-template {
|
||||
.active-card {
|
||||
border: 2px solid transparent;
|
||||
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background: linear-gradient(
|
||||
var(--color-agent-list-selected-bg),
|
||||
var(--color-agent-list-selected-bg)
|
||||
)
|
||||
padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
/* 1. 定义流动动画:让虚线沿路径移动 */
|
||||
@keyframes flowAnimation {
|
||||
to {
|
||||
stroke-dashoffset: 8; /* 与stroke-dasharray总和一致 */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flowAnimationReverse {
|
||||
to {
|
||||
stroke-dashoffset: -8; /* 与stroke-dasharray总和一致 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 2. 为jsPlumb连线绑定动画:作用于SVG的path元素 */
|
||||
/* jtk-connector是jsPlumb连线的默认SVG类,path是实际的线条元素 */
|
||||
.jtk-connector-output path {
|
||||
/* 定义虚线规则:线段长度5px + 间隙3px(总长度8px,与动画偏移量匹配) */
|
||||
stroke-dasharray: 5 3;
|
||||
/* 应用动画:名称+时长+线性速度+无限循环 */
|
||||
animation: flowAnimationReverse 0.5s linear infinite;
|
||||
/* 可选:设置线条基础样式(颜色、宽度) */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* 2. 为jsPlumb连线绑定动画:作用于SVG的path元素 */
|
||||
/* jtk-connector是jsPlumb连线的默认SVG类,path是实际的线条元素 */
|
||||
.jtk-connector-input path {
|
||||
/* 定义虚线规则:线段长度5px + 间隙3px(总长度8px,与动画偏移量匹配) */
|
||||
stroke-dasharray: 5 3;
|
||||
/* 应用动画:名称+时长+线性速度+无限循环 */
|
||||
animation: flowAnimationReverse 0.5s linear infinite;
|
||||
/* 可选:设置线条基础样式(颜色、宽度) */
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
/* 可选: hover时增强动画效果(如加速、变色) */
|
||||
.jtk-connector path:hover {
|
||||
animation-duration: 0.5s; /* hover时流动加速 */
|
||||
}
|
||||
</style>
|
||||
11
frontend/src/__tests__/App.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import App from '../App.vue'
|
||||
|
||||
describe('App', () => {
|
||||
it('mounts renders properly', () => {
|
||||
const wrapper = mount(App)
|
||||
expect(wrapper.text()).toContain('You did it!')
|
||||
})
|
||||
})
|
||||
74
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/request'
|
||||
import type { Agent, IRawPlanResponse } from '@/stores'
|
||||
|
||||
export interface ActionHistory {
|
||||
ID: string
|
||||
ActionType: string
|
||||
AgentName: string
|
||||
Description: string
|
||||
ImportantInput: string[]
|
||||
Action_Result: string
|
||||
}
|
||||
|
||||
export type IExecuteRawResponse = {
|
||||
LogNodeType: string
|
||||
NodeId: string
|
||||
InputName_List?: string[] | null
|
||||
OutputName?: string
|
||||
content?: string
|
||||
ActionHistory: ActionHistory[]
|
||||
}
|
||||
|
||||
class Api {
|
||||
// 智能体信息
|
||||
setAgents = (data: Pick<Agent, 'Name' | 'Profile'>[]) => {
|
||||
return request({
|
||||
url: '/setAgents',
|
||||
data,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
generateBasePlan = (data: { goal: string; inputs: string[] }) => {
|
||||
return request<unknown, IRawPlanResponse>({
|
||||
url: '/generate_basePlan',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'General Goal': data.goal,
|
||||
'Initial Input Object': data.inputs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
executePlan = (plan: IRawPlanResponse) => {
|
||||
return request<unknown, IExecuteRawResponse[]>({
|
||||
url: '/executePlan',
|
||||
method: 'POST',
|
||||
data: {
|
||||
RehearsalLog: [],
|
||||
num_StepToRun: null,
|
||||
plan: {
|
||||
'Initial Input Object': plan['Initial Input Object'],
|
||||
'General Goal': plan['General Goal'],
|
||||
'Collaboration Process': plan['Collaboration Process']?.map(step => ({
|
||||
StepName: step.StepName,
|
||||
TaskContent: step.TaskContent,
|
||||
InputObject_List: step.InputObject_List,
|
||||
OutputObject: step.OutputObject,
|
||||
AgentSelection: step.AgentSelection,
|
||||
Collaboration_Brief_frontEnd: step.Collaboration_Brief_FrontEnd,
|
||||
TaskProcess: step.TaskProcess.map(action => ({
|
||||
ActionType: action.ActionType,
|
||||
AgentName: action.AgentName,
|
||||
Description: action.Description,
|
||||
ID: action.ID,
|
||||
ImportantInput: action.ImportantInput,
|
||||
})),
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default new Api()
|
||||
1
frontend/src/assets/icons/action.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761736278335" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5885" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M226.592 896C167.616 896 128 850.48 128 782.736V241.264C128 173.52 167.616 128 226.592 128c20.176 0 41.136 5.536 62.288 16.464l542.864 280.432C887.648 453.792 896 491.872 896 512s-8.352 58.208-64.272 87.088L288.864 879.536C267.712 890.464 246.768 896 226.592 896z m0-704.304c-31.008 0-34.368 34.656-34.368 49.568v541.472c0 14.896 3.344 49.568 34.368 49.568 9.6 0 20.88-3.2 32.608-9.248l542.864-280.432c21.904-11.328 29.712-23.232 29.712-30.608s-7.808-19.28-29.712-30.592L259.2 200.96c-11.728-6.048-23.008-9.264-32.608-9.264z" p-id="5886"></path></svg>
|
||||
|
After Width: | Height: | Size: 886 B |
1
frontend/src/assets/icons/agent-change.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764640542560" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1335" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M838.611638 631.662714l67.309037-44.87798 87.97131 131.929203a40.41239 40.41239 0 0 1-33.646587 62.843448H555.939064v-80.82478h328.851364l-46.17879-69.069891zM718.135918 979.106157l-67.237652 44.862116-88.050627-131.921271a40.41239 40.41239 0 0 1 33.583133-62.779994h404.37772v80.761326h-328.772047l46.107404 69.149209v-0.071386zM701.510919 407.10625a294.54644 294.54644 0 0 0-84.695487-59.139309c44.34655-35.891279 72.670917-90.69984 72.670917-152.218682 0-108.300447-90.461886-197.31875-199.008219-195.621351A196.089325 196.089325 0 0 0 318.390354 107.673837a7.828661 7.828661 0 0 0 6.202648 11.278983c18.655533 2.014671 36.882751 6.194716 54.126428 12.214932a7.804866 7.804866 0 0 0 9.07395-3.140982 125.203058 125.203058 0 0 1 104.11247-57.632273 124.608175 124.608175 0 0 1 91.262995 37.898018 124.679561 124.679561 0 0 1 35.462963 83.759537 124.846128 124.846128 0 0 1-73.979659 117.953417 7.884184 7.884184 0 0 0-4.386271 8.582179 250.96134 250.96134 0 0 1 2.879234 71.409765l0.13484 0.063454a7.828661 7.828661 0 0 0 5.821923 8.526657 220.661962 220.661962 0 0 1 102.478524 58.369928 221.201323 221.201323 0 0 1 65.278503 150.338851 7.773139 7.773139 0 0 0 7.828661 7.51139h55.189286a7.844525 7.844525 0 0 0 7.574845-8.074546 291.05646 291.05646 0 0 0-85.940775-199.626897z" fill="#17A29E" p-id="1336"></path><path d="M458.386171 627.942712h89.145212a291.072323 291.072323 0 0 0-41.649747-52.38937 293.443924 293.443924 0 0 0-84.568578-59.13931A195.875168 195.875168 0 0 0 493.761885 367.550491c1.808445-108.173539-84.425806-197.310819-192.591413-199.111331l-0.824905-0.007932c-108.768422-0.650405-197.469454 86.987769-198.119859 195.764123a195.296148 195.296148 0 0 0 72.655053 152.21075c-31.441554 14.602397-59.401058 35.288464-84.560647 59.139309C4.371408 657.03646 6.187784 763.131873 4.371408 838.277504a7.836593 7.836593 0 0 0 7.828661 8.011092h54.816492a7.804866 7.804866 0 0 0 7.828662-7.511391c1.840172-56.601142 25.207179-173.483769 65.334025-213.420252 42.157381-42.157381 98.227094-65.334025 157.850241-65.334025a221.827933 221.827933 0 0 1 157.858173 65.334025c0.864563 0.832836 1.657741 1.729127 2.498509 2.585759zM298.037421 489.549113l-0.063454-0.071386a124.663697 124.663697 0 0 1-88.637578-36.636866c-23.668415-23.747732-36.70032-55.141695-36.70032-88.64551 0-33.829018 13.27779-65.643364 37.580747-89.454551a126.05969 126.05969 0 0 1 89.081757-35.764371 125.512397 125.512397 0 0 1 86.686362 36.414776c49.169069 48.820071 49.454613 128.264723 0.642474 177.449656a124.354358 124.354358 0 0 1-88.589988 36.708252z" fill="#17A29E" p-id="1337"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/src/assets/icons/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764755052115" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6234" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M886.784 746.496q29.696 30.72 43.52 56.32t-4.608 58.368q-4.096 6.144-11.264 14.848t-14.848 16.896-15.36 14.848-12.8 9.728q-25.6 15.36-60.416 8.192t-62.464-34.816l-43.008-43.008-57.344-57.344-67.584-67.584-73.728-73.728-131.072 131.072q-60.416 60.416-98.304 99.328-38.912 38.912-77.312 48.128t-68.096-17.408l-7.168-7.168-11.264-11.264-11.264-11.264q-6.144-6.144-7.168-8.192-11.264-14.336-13.312-29.184t2.56-29.184 13.824-27.648 20.48-24.576q9.216-8.192 32.768-30.72l55.296-57.344q33.792-32.768 75.264-73.728t86.528-86.016q-49.152-49.152-93.696-93.184t-79.872-78.848-57.856-56.832-27.648-27.136q-26.624-26.624-27.136-52.736t17.92-52.736q8.192-10.24 23.552-24.064t21.504-17.92q30.72-20.48 55.296-17.92t49.152 28.16l31.744 31.744q23.552 23.552 58.368 57.344t78.336 76.288 90.624 88.576q38.912-38.912 76.288-75.776t69.632-69.12 58.368-57.856 43.52-43.008q24.576-23.552 53.248-31.232t55.296 12.8q1.024 1.024 6.656 5.12t11.264 9.216 10.752 9.728 7.168 5.632q27.648 26.624 27.136 57.856t-27.136 57.856q-18.432 18.432-45.568 46.08t-60.416 60.416-70.144 69.632l-77.824 77.824q37.888 36.864 74.24 72.192t67.584 66.048 56.32 56.32 41.472 41.984z" p-id="6235"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
frontend/src/assets/icons/doctor.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212882014" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3052" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M988.8 939.2c0-134.4-78.4-252.8-193.6-308.8 4.8 12.8 8 25.6 8 40v11.2c54.4 11.2 96 60.8 96 118.4v65.6c8 6.4 14.4 17.6 14.4 30.4 0 22.4-17.6 38.4-38.4 38.4-22.4 0-38.4-17.6-38.4-38.4 0-12.8 6.4-24 16-32v-67.2c0-40-32-72-72-72s-72 32-72 72v68.8c9.6 6.4 14.4 17.6 14.4 30.4 0 22.4-17.6 38.4-38.4 38.4-22.4 0-38.4-17.6-38.4-38.4 0-11.2 4.8-22.4 14.4-30.4V800c0-59.2 43.2-108.8 100.8-120-1.6-28.8-14.4-56-32-76.8-19.2-4.8-38.4-8-57.6-8-9.6 216-75.2 384-156.8 384-80 0-147.2-168-156.8-384h-1.6c-19.2 22.4-32 52.8-32 84.8v99.2c27.2 9.6 48 36.8 48 68.8 0 40-32 73.6-73.6 73.6s-73.6-32-73.6-73.6c0-33.6 22.4-60.8 52.8-70.4v-118.4c0-19.2 6.4-38.4 16-54.4C145.6 644.8 35.2 779.2 35.2 939.2V953.6C35.2 992 249.6 1024 512 1024s476.8-32 476.8-72v-3.2-9.6z m-470.4-720C400 219.2 304 256 304 300.8v4.8l44.8 212.8c0 3.2 1.6 8 1.6 11.2 19.2 75.2 86.4 129.6 166.4 129.6 84.8 0 156.8-64 169.6-145.6l44.8-208v-4.8c0-44.8-96-81.6-212.8-81.6z m-1.6-16c105.6 0 193.6 35.2 225.6 83.2h9.6V83.2v-1.6C752 36.8 646.4 0 518.4 0S284.8 36.8 284.8 81.6V288h6.4c30.4-49.6 120-84.8 225.6-84.8z m-65.6-105.6c0-8 6.4-16 16-16h33.6V46.4c0-8 6.4-16 16-16 8 0 16 6.4 16 16V80h33.6c8 0 16 6.4 16 16s-6.4 16-16 16h-33.6v33.6c0 8-6.4 16-16 16-8 0-16-6.4-16-16v-32h-33.6c-8 0-16-8-16-16z" p-id="3053"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/src/assets/icons/engineer.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212791945" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2409" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M338.36032 362.9056c9.59488 59.4944 34.53952 115.1488 74.84416 161.20832l15.34976 13.43488c1.23904 2.47808 3.2768 4.15744 5.60128 6.07232 1.26976 1.04448 2.63168 2.16064 3.9936 3.52256 42.21952 38.37952 107.4688 38.37952 147.7632-1.92512 53.73952-47.9744 80.60928-113.22368 95.95904-182.31296H338.37056z m366.5408-63.32416h26.86976c7.68 0 13.43488 5.75488 9.59488 11.50976v15.36H275.02592v-15.36c0-7.68 5.75488-13.43488 13.43488-13.43488h28.78464c1.09568-7.63904 2.02752-14.97088 2.93888-22.12864 2.304-17.99168 4.4544-34.8672 8.58112-52.71552 5.75488-34.53952 21.10464-69.08928 42.21952-95.9488 21.10464-24.95488 49.88928-44.1344 82.51392-49.89952 40.30464-9.59488 82.5344-9.59488 120.90368 1.91488 47.98464 11.52 86.36416 46.05952 103.6288 92.11904 13.44512 36.46464 23.04 72.92928 24.95488 111.3088v7.68c1.91488 3.84 1.91488 5.75488 1.91488 9.59488z m-241.80736-86.36416c7.68-1.91488 13.43488-9.59488 13.43488-17.26464V132.608c0-7.68-7.68-15.36-15.34976-15.36-7.68 0-15.36 7.68-15.36 15.36v65.24928c0 7.68 7.68 15.36 15.36 15.36h1.91488z m99.79904 0c7.68-1.91488 13.43488-9.59488 13.43488-17.26464V132.608c0-7.68-7.68-15.36-15.36-15.36s-15.34976 7.68-15.34976 15.36v65.24928c0 7.68 7.68 15.36 15.36 15.36h1.91488z m351.16032 596.8384c5.8368 20.5824 11.66336 41.08288 17.3056 61.42976 4.1984 19.56864 9.41056 37.09952 14.8992 55.57248 2.048 6.88128 4.12672 13.89568 6.21568 21.1968 1.91488 3.82976 0 5.75488-3.84 5.75488H73.5232c-1.91488 0-3.84-3.84-1.91488-5.76512 3.82976-13.43488 7.68-25.9072 11.50976-38.37952 3.84-12.47232 7.68-24.94464 11.52-38.37952 12.1856-48.7424 26.27584-98.44736 40.26368-147.75296 5.8368-20.5824 11.65312-41.08288 17.3056-61.44 1.91488-7.66976 3.84-9.58464 11.50976-11.50976 18.2272-3.82976 35.98336-8.15104 53.73952-12.47232 17.75616-4.31104 35.50208-8.63232 53.73952-12.47232 3.82976-1.91488 5.75488 0 5.75488 3.84v163.1232c0 1.91488 1.91488 3.84 3.84 3.84h47.9744a4.12672 4.12672 0 0 0 3.84-3.84V612.4032c2.2528 0 3.1744-0.65536 3.95264-1.19808 0.54272-0.38912 1.00352-0.7168 1.80224-0.7168 23.02976-3.84 46.05952-17.27488 59.4944-38.37952 1.91488-3.84 5.75488-7.68 9.59488-11.52 1.91488-1.91488 5.75488-1.91488 7.68 0 15.34976 15.36 28.7744 30.69952 42.20928 46.05952 13.43488 17.27488 34.54976 26.86976 55.6544 24.94464 21.11488 1.92512 42.22976-7.68 55.6544-24.94464 13.43488-15.36 26.86976-30.70976 42.22976-46.05952 1.91488-1.91488 5.75488-1.91488 7.68 0 1.91488 3.84 5.74464 7.68 9.58464 11.52 13.43488 21.10464 34.54976 34.53952 59.4944 38.37952 1.92512 0 3.84 0 5.75488 1.91488v180.39808c0 1.91488 1.92512 3.84 3.84 3.84h47.9744a4.12672 4.12672 0 0 0 3.84-3.84v-163.1232c0-1.92512 3.84-3.84 5.76512-3.84 36.4544 9.59488 71.00416 17.27488 107.4688 24.94464a17.3056 17.3056 0 0 1 11.50976 11.52c12.1856 48.7424 26.28608 98.44736 40.26368 147.75296z" p-id="2410"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
14
frontend/src/assets/icons/icons.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
className="icon"
|
||||
viewBox="0 0 8960 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="9634"
|
||||
width="100%"
|
||||
height="60"
|
||||
>
|
||||
<path
|
||||
d="M8960 0c-451.52 181.184-171.2 1024-992 1024H992C171.232 1024 451.392 181.184 0 0h8960z"
|
||||
p-id="9635"
|
||||
></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
1
frontend/src/assets/icons/left.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764757574909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9019" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M248.88888875 558.88888844l401.7777778 401.77777782c28.44444469 28.44444469 74.66666625 28.44444469 99.55555594 0 28.44444469-28.44444469 28.44444469-74.66666625 1e-8-99.555555l-352.00000031-352.00000032 352.00000031-351.99999937c28.44444469-28.44444469 28.44444469-71.11111125 0-99.55555594s-74.66666625-28.44444469-99.55555594 0L248.88888875 459.33333344c-14.22222188 14.22222188-21.33333375 31.99999969-21.33333281 49.7777775 0 17.7777778 7.11111094 35.55555562 21.33333281 49.7777775" fill="#ffffff" p-id="9020"></path></svg>
|
||||
|
After Width: | Height: | Size: 860 B |
1
frontend/src/assets/icons/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761626283461" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2759" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 1 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z" fill="" p-id="2760"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/src/assets/icons/medical.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212815712" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2569" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M845.285053 1008.842105H520.421053v-52.224c45.999158-24.468211 96.538947-287.420632 134.03621-286.302316 145.960421 30.100211 249.317053 113.057684 249.317053 270.928843-1.684211 41.472-21.005474 67.597474-58.489263 67.597473z m0 0" p-id="2570"></path><path d="M719.764211 480.619789c-32.363789 107.344842-116.439579 183.457684-214.662737 183.457685-98.829474 0-182.905263-76.665263-214.703158-184.010106-14.767158-6.831158-32.943158-22.150737-39.760842-61.291789-8.528842-51.186526 22.703158-63.690105 22.703158-63.690105 2.290526 0 4.554105 1.751579 6.817684 5.133473a95.137684 95.137684 0 0 1 24.427789-36.917894c40.313263-39.774316 114.714947-60.779789 204.463158-61.359158 81.232842-0.565895 138.024421 5.133474 195.961263 60.240842 10.213053 9.633684 19.887158 24.939789 24.980211 40.326737 2.842947-4.554105 5.685895-7.424 8.528842-6.817685 0 0 31.797895 12.476632 23.282526 63.609264-7.949474 39.733895-26.691368 54.501053-42.037894 61.318736z m0 0" p-id="2571"></path><path d="M496.559158 15.454316c-126.059789 0-231.141053 64.794947-231.141053 64.794947v215.794526s69.308632-78.942316 237.406316-78.942315c166.979368 0 237.406316 79.508211 237.406316 79.50821V80.801684c-0.579368-0.552421-122.677895-65.347368-243.671579-65.347368z m71.545263 117.005473h-52.237474v52.237474h-27.823158V132.459789h-51.685052V104.084211h51.685052V51.846737h27.823158v52.237474h52.237474v28.375578zM497.125053 956.618105v52.224c-141.433263 0-240.249263-0.538947-324.877474 0-36.352 0-56.212211-26.125474-58.489263-67.058526 0-157.884632 103.356632-241.367579 249.330526-270.901895 38.642526-1.684211 88.616421 261.268211 134.036211 285.736421z" p-id="2572"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
1
frontend/src/assets/icons/moon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764820450888" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23116" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M854.442667 725.376c-117.845333 204.117333-378.837333 274.048-582.954667 156.202667A425.173333 425.173333 0 0 1 133.546667 754.346667a32 32 0 0 1 15.573333-48.298667c160.725333-57.514667 246.826667-124.16 296.789333-219.562667 52.565333-100.394667 66.176-210.346667 29.397334-361.088a32 32 0 0 1 32.810666-39.552 425.002667 425.002667 0 0 1 190.165334 56.618667c204.117333 117.845333 274.048 378.837333 156.16 582.912z" fill="#ffffff" p-id="23117"></path></svg>
|
||||
|
After Width: | Height: | Size: 793 B |
1
frontend/src/assets/icons/paper-plane.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761204835005" class="icon" viewBox="0 0 1171 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5692" xmlns:xlink="http://www.w3.org/1999/xlink" width="228.7109375" height="200"><path d="M502.237757 1024 644.426501 829.679301 502.237757 788.716444 502.237757 1024 502.237757 1024ZM0 566.713817 403.967637 689.088066 901.485385 266.66003 515.916344 721.68034 947.825442 855.099648 1170.285714 0 0 566.713817 0 566.713817Z" p-id="5693"></path></svg>
|
||||
|
After Width: | Height: | Size: 603 B |
1
frontend/src/assets/icons/plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761211358508" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6686" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" p-id="6687"></path></svg>
|
||||
|
After Width: | Height: | Size: 516 B |
1
frontend/src/assets/icons/process.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
frontend/src/assets/icons/refresh.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761368152518" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7696" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M835.516 709.837l154.351-225.894h-109.158c-14.541-221.047-198.11-396.151-422.844-396.151-234.291 0-423.731 189.918-423.731 424.277 0 234.155 189.918 424.141 424.209 424.141 105.062 0 200.977-38.434 275.115-101.786l-56.73-73.045c-58.368 51.063-134.69 82.398-218.385 82.398-183.296 0-331.844-148.617-331.844-331.708 0-183.364 148.617-331.913 331.844-331.913 173.739 0 315.665 133.734 329.933 303.787h-107.11l154.351 225.894z" p-id="7697"></path></svg>
|
||||
|
After Width: | Height: | Size: 783 B |
1
frontend/src/assets/icons/renyuan.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761545116983" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1453" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M856.98894 727.680806c-64.797424-20.50007-128.161277-40.856783-169.448131-61.356852-28.814784-14.335713-46.877782-28.671427-46.877782-43.00714v-34.405712c10.034999-8.171357 19.209856-17.202856 27.381212-27.237855 2.293714-2.867143 4.587428-5.734285 6.881142-8.888143 10.034999-13.905642 20.213356-32.972141 29.101498-51.895282 9.891642-21.073499 18.349713-41.716926 23.223856-54.47571 4.730785-5.590928 8.888142-11.181856 12.758785-16.629428 22.363713-32.111998 30.104998-62.647067 23.223855-91.175136-3.870643-16.48607-12.902142-31.395212-25.804284-42.433712 5.0175-40.570069 4.730785-86.444351-10.034999-126.727705-7.597928-21.216856-18.349713-41.143497-31.968641-59.063139-20.643427-26.807784-47.451211-47.307854-80.279994-61.070138-42.00364-17.632927-81.856923-19.639927-97.196136-19.639927h-10.608428c-15.195856 0-55.049139 1.863643-97.196136 19.639927-35.122498 14.765785-63.363853 37.129497-84.580708 66.947781-11.611928 16.48607-20.930141 34.405712-27.667927 53.328853-14.765785 40.283354-15.052499 86.300994-10.034999 126.727706-12.758785 11.038499-21.790284 25.947641-25.804284 42.433711-6.737785 27.954641 0.716786 57.916282 22.076998 89.454851 4.157357 6.164357 8.888142 12.185356 14.048999 18.49307 5.0175 12.902142 13.332213 33.545569 23.223856 54.762425 8.888142 18.923142 18.923142 37.846283 28.958141 51.608568 2.150357 3.0105 4.444071 5.877642 6.737785 8.744785 7.741285 9.461571 16.48607 18.206356 25.947641 25.947641v35.695926c0 14.47907-18.49307 29.101498-48.02464 43.580568-41.286854 20.213356-104.220636 40.570069-168.301273 60.783425C56.626067 762.51659 56.626067 946.157077 56.626067 946.157077s134.038919 48.884782 457.165897 48.884782S966.943861 946.157077 966.943861 946.157077s0.286714-183.783844-109.954921-218.476271zM351.224976 414.875542c-46.877782-51.321854-28.958141-72.251995-18.349714-76.265994 17.48957-6.594428 29.388212 19.49657 33.54557 31.108498-4.874143-15.052499-18.062999-59.636567-20.213356-106.51435 15.769285-22.076998 47.164497-58.919782 84.580708-64.797424 0 0 80.710066 96.47935 246.574269 94.47235-3.727285 26.090998-9.604928 51.895282-17.632928 76.982781 4.300714-11.611928 16.055999-37.702926 33.545569-31.108498 10.608428 4.014 28.528069 25.087498-18.349713 76.265995 0 0-17.346213 47.164497-35.552568 80.99678-3.870643 7.454571-8.171357 14.47907-12.902142 21.360212-12.041999 16.48607-27.667927 29.818284-45.874283 38.993141-20.50007 10.465071-43.293854 15.912642-66.230995 16.055998-0.430071 0-0.860143 0-1.146857-0.143357-0.430071 0-0.860143 0.143357-1.146857 0.143357-23.653927-0.143357-47.02114-6.021-68.094639-17.059498a129.42282 129.42282 0 0 1-44.010639-38.132998c-4.730785-6.737785-9.031499-13.762285-12.902142-21.073498-18.349713-33.832283-35.839283-81.283494-35.839283-81.283495z m170.881702 519.52625c-5.734285 3.727285-9.174857 5.447571-9.174857 5.447571s-3.440571-1.863643-9.174856-5.447571c-31.968641-20.069999-137.336133-94.615708-151.815204-208.584628 34.405712-15.48257 91.891922-46.447711 91.891922-102.50035v-2.293714c21.933641 7.454571 44.870783 11.468571 67.951281 11.755284h2.293714c22.50707-0.143357 44.870783-4.014 66.230996-11.181856v1.720286c0 57.056139 59.49321 88.164637 93.612208 103.360492-14.909142 113.252135-119.98992 187.654487-151.815204 207.724486z" p-id="1454"></path><path d="M574.432031 693.418452l-28.097998-30.53507h-66.804424L451.574969 693.418452s16.055999 34.118998 42.003639 47.451211l-42.003639 106.084278s30.821784 43.723926 61.356852 55.049139c30.678426-11.325213 61.50021-55.049139 61.50021-55.049139l-42.00364-106.084278c25.804284-13.332213 42.00364-47.451211 42.00364-47.451211z" p-id="1455"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
1
frontend/src/assets/icons/researcher.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212863001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2892" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M725.308 314.532c2.743 1.543 5.486 3.215 8.164 5.057l8.164 5.55 0.15 9.77c0.45 28.328-2.957 52.326-10.37 70.796-6.836 16.885-16.971 29.527-30.556 37.134-9.235 32.976-20.077 63.532-35.42 89.18-17.677 29.548-41.011 52.261-74.01 64.496-14.763 5.4-52.432 7.907-88.708 7.393-35.012-0.536-70.131-4.007-84.06-10.414-29.997-13.842-51.082-37.048-67.045-65.975-13.692-24.705-23.463-53.46-31.97-83.823-14.034-7.328-24.577-19.906-31.669-36.92-7.778-18.577-11.314-42.982-10.82-71.866l0.106-9.814 8.143-5.485c2.1-1.414 4.178-2.722 6.256-3.921-9.17-113.544-5.72-155.52 36.448-203.495 82.217-67.346 270.712-64.968 354-3.943 56.718 53.547 60.66 113.586 43.197 206.28m-172.66 328.33l1.2 26.013-15.407 25.434 21.47 141.12 88.045-189.224 134.07-4.585c69.189 65.503 113.63 219.758 102.701 320.38H137.623c1.843-88.366 18.106-239.278 106.108-316.03l121.107 1.135 113.414 187.124 21.32-139.92-15.427-25.434 1.178-26.013c29.355-1.607 37.99-1.607 67.325 0m100.3-368.656c-53.246 10.414-132.57 19.52-195.245-15.706-24.105-13.563-59.417 14.228-88.301 11.378a217.808 217.808 0 0 0-19.542 57.682l-3.214 17.035-17.142-1.671a24.02 24.02 0 0 0-9.942 1.264 38.098 38.098 0 0 0-4.65 1.843c0.45 18.877 3.107 34.54 7.971 46.261 4.285 10.307 10.307 17.035 18.106 19.477l10.007 3.107 2.742 10.071c8.4 31.134 17.806 60.468 31.027 84.36 12.256 22.22 27.984 39.833 49.711 49.84 9.107 4.156 38.226 6.556 68.653 7.006 32.334 0.471 64.603-1.243 75.253-5.164 23.934-8.871 41.204-25.97 54.618-48.34 14.399-24.127 24.62-54.661 33.62-88.173l2.549-9.578 9.535-3.321c7.542-2.7 13.392-9.536 17.549-19.97 4.714-11.614 7.264-27.02 7.692-45.534a35.355 35.355 0 0 0-4.178-1.67 25.413 25.413 0 0 0-9.706-1.48l-16.67 1.072-3.108-16.435a213.844 213.844 0 0 0-17.334-53.354m0 0" p-id="2893"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
1
frontend/src/assets/icons/right.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764757483582" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7204" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M775.11111125 465.11111156l-401.7777778-401.77777782c-28.44444469-28.44444469-74.66666625-28.44444469-99.55555594 0-28.44444469 28.44444469-28.44444469 74.66666625-1e-8 99.555555l352.00000031 352.00000032-352.00000031 351.99999937c-28.44444469 28.44444469-28.44444469 71.11111125 0 99.55555594s74.66666625 28.44444469 99.55555594 0L775.11111125 564.66666656c14.22222188-14.22222188 21.33333375-31.99999969 21.33333281-49.7777775 0-17.7777778-7.11111094-35.55555562-21.33333281-49.7777775" fill="#d8d8d8" p-id="7205"></path></svg>
|
||||
|
After Width: | Height: | Size: 860 B |
1
frontend/src/assets/icons/shejishi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761545158610" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1618" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 625.777778l85.333333 85.333333-56.888889 85.333333 56.888889 142.222223 79.644445-290.133334c142.222222 34.133333 290.133333 102.4 290.133333 204.8v113.777778H56.888889v-113.777778c0-108.088889 147.911111-176.355556 290.133333-204.8l79.644445 290.133334 56.888889-142.222223-56.888889-85.333333L512 625.777778z m196.266667-261.688889c0 110.933333-85.333333 204.8-196.266667 204.8-107.235556 0-190.577778-87.722667-195.982222-193.763556L315.733333 364.088889h392.533334zM521.159111 56.888889c12.970667 0.170667 28.444444 0.967111 41.415111 4.949333l9.159111 3.584v136.533334h34.133334v-119.466667c65.024 32.483556 114.574222 103.708444 119.125333 184.149333l0.341333 12.117334h42.666667v42.666666h-512V278.755556h42.666667c0-81.066667 38.513778-154.453333 100.864-190.805334L409.6 82.488889v119.466667h42.666667v-136.533334c14.222222-7.111111 34.360889-8.305778 50.574222-8.533333h18.318222z" p-id="1619"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
1
frontend/src/assets/icons/soldier.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212898988" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3212" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M105.9 959.3c5.5-79.7 16.6-187.3 41.1-286.1 5.1-25.5 23.2-46.9 23.8-47.7 2.3-15.1 187.9-15.2 187.9-15.2l30.7-2.9c-4.4 27.2-7.7 68.5 4 104 15.2 46.6 39.4 92.8 72.3 120.3l7-123.7H536l10.9 116c23-32.9 49.8-84.7 52.6-150.7 2.2-51.6 15.1-75.2 26.2-86.1 76.9 3.1 173.3 17.2 212 38.4 36.9 20.2 48.5 146.4 81.9 333.8H105.9v-0.1z" p-id="3213"></path><path d="M528.1 690.2h-39.7l-31.8-26.6 47.7-62 47.6 53.2-23.8 35.4z m210.7-526.8c-19-38-129.4-95-249.8-98.9-120.4-4-247.9 65.9-246.9 117 1 51 64.2 73 64.2 73-3.3 9.9-1.6 48.3-1.6 48.3s2.9 5.3 8.4 12.7C294.5 560.8 509 565 509 565c185.9-19.3 171.1-221.2 169.3-250.4-0.7-10.7-2-19.2-3.6-26.8 2.3-16.5-4.1-33.5-4.1-33.5 27.1-10.9 87.3-52.9 68.2-90.9z m-280.2-19.1c-0.2-1.6-0.5-3.2-0.5-4.9 0-20.7 18.2-37.5 40.7-37.5 22.5 0 40.7 16.8 40.7 37.5 0 2.6-0.3 5.2-0.9 7.7 12.1-1.1 25.2-2.1 37.1-2.3 0 0-33.9 15.8-53.2 25.1-2.9 1.9-6 3.4-9.4 4.6-0.2 0.1-0.5 0.3-0.8 0.4 0 0-1.1 0.3-2.9 0.6-3.4 0.9-7 1.5-10.7 1.5-0.7 0-1.4-0.2-2.1-0.2-9 0-20.4-1.8-29.6-9.5 0 0-13.9-9.5-45.3-22.6 0.1 0 16.8-2.1 36.9-0.4z m181.3 107.8H332v-36.4h307.9v36.4z" p-id="3214"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/src/assets/icons/specialist.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212837698" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2731" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M855.7 781.7c-11.8-25.2-29.1-47.3-48.4-67.2-26.3-27-56.5-50-87.9-70.7-32.1-20.9-65.6-39.7-100.4-55.6 28.1-18.7 49.6-45.9 64.9-75.7 20.9-40.7 31.9-85.6 38.1-130.7 6.3-47.9 7.5-96.5 3.7-144.7 32.3-12.9 64.5-25.8 96.8-38.7 12-5 24.3-9.4 36.2-14.7-114.9-39.9-229.9-79.6-344.8-119.4-2-1-4 0.5-5.9 1-108.1 39.5-216.3 78.9-324.5 118.4 39.4 18.4 79 36.5 118.5 54.9-8.8 55.8-7.2 113.4 7.1 168.2 9 34.8 23 68.2 41.4 99.1 18.3 30.4 40.2 58.9 67 82.3-48 15.6-94.7 36.5-136.1 65.7-28.4 20-54.1 43.9-74.7 71.9-23 31.1-39.5 67.3-46.7 105.4-3.7 19.5-5.3 39.5-3.2 59.2 112.7 48 235.5 71.4 357.9 69.7-37.9-32.3-75.9-64.6-113.9-96.9 30.3-73.3 60.5-146.6 90.8-219.9 0.4-1.4 1.8-2.9 0.8-4.2-8.6-14.2-17.4-28.2-25.8-42.5 14.6 7.7 30.8 12.6 47.4 12.7 14.9 0.3 29.7-4.3 42-12.7-9.1 13.7-18.3 27.4-27.5 41.1 17.8 40.6 35.7 81.2 53.5 121.9 14.7 33.9 29.9 67.6 44.4 101.5-37.3 32.9-74.3 66.1-111.4 99.2 90.1-1.2 180-15.1 266.2-41.2 26.5-8.2 52.7-17.2 78-28.6 6.5-15.2 10.3-31.6 10.5-48.1 0.2-21-5.2-41.8-14-60.7z" p-id="2732"></path><path d="M216.4 380.8h11V392h21.5v-11.2h11v-26.3h-11V223.3h-21.5c-0.1 43.7 0 87.5 0 131.2h-11v26.3z" p-id="2733"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/src/assets/icons/sunny.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1764820260653" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13095" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M152.00256 792.00256l76.00128-77.99808 60.00128 60.00128-76.00128 77.99808z m317.99808 165.99552V832h83.99872v125.99808H470.00064zM512 233.99936c141.99808 0 256 114.00192 256 256s-114.00192 256-256 256-256-114.00192-256-256 114.00192-256 256-256zM854.00064 448h128v86.00064h-128V448z m-118.00064 326.00064l60.00128-57.99936 76.00128 76.00128-60.00128 60.00128z m136.00256-584.00256l-76.00128 76.00128-60.00128-60.00128 76.00128-76.00128z m-318.0032-165.99552v125.99808H470.00064V24.00256h83.99872z m-384 423.99744v86.00064h-128V448h128zM288 205.99808L227.99872 265.99936 151.99744 189.99808 211.99872 129.9968z" p-id="13096" fill="#ffa300"></path></svg>
|
||||
|
After Width: | Height: | Size: 985 B |
1
frontend/src/assets/icons/technicist.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1761212761688" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2247" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M636.478451 965.736274l10.285126-0.276102c-2.068945-0.25056-5.512321-0.662889-10.064973-1.188336a398.001259 398.001259 0 0 1-0.220153 1.464438zM319.897045 966.147387l15.81934-0.411113-0.214071-1.463222c-7.760064 0.893989-12.318797 1.463222-12.318797 1.463222l-3.286472 0.411113zM701.706773 232.246056c0-130.470023-177.671401-195.699562-177.671401-195.699562-25.16304-11.331153-50.139985-8.750141-75.875908 0 0 0-177.679915 57.274865-177.679915 195.699562 0 20.694314-37.558465 16.252347-49.98673 81.7142h531.217713c-22.614869-65.914321-50.003758-61.019886-50.003759-81.7142zM261.149228 691.016641C179.668559 732.292152 110.143011 786.116343 110.143011 839.449144v27.720942c0 57.176344 150.840799 106.349362 150.840799 106.349362l58.913235-7.372061-5.835859 0.152039s-48.965029-277.278751-52.911958-275.282785z" p-id="2248"></path><path d="M649.282557 868.865624c-18.205742 19.018238-43.489197 24.747062-67.840958 15.467826-25.402654-9.681835-41.173341-37.079238-36.441892-62.991526 2.217335-12.137567 7.629918-22.585677 16.273024-31.609489 36.338505-37.95255 72.662414-75.917262 108.994838-113.872244-20.740534-9.049353-41.343624-19.515709-62.035506-26.357458-5.480697-1.774598-39.908378-17.257019-18.379674-82.532777h-0.291915c50.639889-51.988778 86.252256-102.79895 86.252256-215.156888h-378.248369c0 113.53411 34.725677 163.877219 85.702483 215.795451 22.010362 57.542453-17.334863 78.908171-25.565639 81.90881-22.185511 8.003326-37.025721 17.671781-59.579774 27.987314-4.070993 1.864605 34.183202 264.833458 37.380883 286.769626 28.691558-3.307149 101.308969-11.070862 150.593888-11.070862 49.355464 0 121.929088 7.763713 150.601185 11.070862 1.212662-8.076305 7.314894-48.820288 14.275842-97.177161l-1.690672 1.768516zM780.85699 731.325185a466047.419516 466047.419516 0 0 1-98.887294 103.3694c-11.765376 66.245158-22.605138 130.427453-22.605138 130.427452l-12.599765 0.336918c1.455924 0.177581 2.246526 0.276103 2.246526 0.276103l62.66069 7.96562s150.381033-49.146259 150.381033-106.531808v-27.719726c-0.002433-37.857677-33.167582-75.24221-81.196052-108.123959z" p-id="2249"></path><path d="M843.295095 519.324624l-52.478951 54.703584 62.663122 57.647056 52.688157-55.292278c0.620319 1.188336 0.975481 1.74419 1.211445 2.343831 20.077644 51.086275-8.213748 109.894908-61.513708 127.951044-20.261307 6.862426-40.735469 7.679787-61.405457 2.17598-2.048268-0.54734-2.96415 0.256642-4.130592 1.474169A361833.588559 361833.588559 0 0 1 643.464941 853.40023c-13.557002 14.162724-32.385495 18.42711-50.519474 11.518465-18.916067-7.210291-30.659549-27.611474-27.138329-46.907031 1.652967-9.039623 5.682605-16.819147 12.118106-23.538049 45.458404-47.47748 90.870589-94.993883 136.371564-142.431225 1.828115-1.904743 2.042186-3.3631 1.162793-5.751934-19.576524-53.126028 12.26163-112.586604 68.353025-127.899958 18.585231-5.074449 37.137621-5.129183 55.717987-0.199475 1.046027 0.278535 2.072594 0.622751 3.764482 1.133601zM604.103904 841.024267c5.432045-0.211638 9.560204-4.471159 9.419112-9.706161-0.147174-5.362715-4.626847-9.473846-10.113626-9.281669-5.391906 0.189745-9.684268 4.732666-9.44587 10.004157 0.233532 5.146212 4.800779 9.190445 10.140384 8.983673z" p-id="2250"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
93
frontend/src/components/MultiLineTooltip/index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<el-tooltip :disabled="!isOverflow" effect="light" placement="top" :content="text">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="multi-line-ellipsis"
|
||||
:style="containerStyle"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<slot>
|
||||
{{ text }}
|
||||
</slot>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, type HTMLAttributes, nextTick, onMounted, ref } from 'vue'
|
||||
import { ElTooltip } from 'element-plus'
|
||||
|
||||
// 定义组件 props 类型
|
||||
interface Props {
|
||||
text?: string
|
||||
lines?: number
|
||||
maxWidth?: string | number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: '',
|
||||
lines: 3,
|
||||
maxWidth: '100%',
|
||||
})
|
||||
|
||||
const isOverflow = ref(false)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 计算容器样式
|
||||
const containerStyle = computed(
|
||||
() =>
|
||||
({
|
||||
maxWidth: props.maxWidth,
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: props.lines,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.5',
|
||||
wordBreak: 'break-all',
|
||||
}) as HTMLAttributes['style'],
|
||||
)
|
||||
|
||||
// 检查文字是否溢出
|
||||
const checkOverflow = (element: HTMLElement): boolean => {
|
||||
// 单行情况下使用宽度判断
|
||||
if (props.lines === 1) {
|
||||
return element.scrollWidth > element.clientWidth
|
||||
}
|
||||
// 多行情况下使用高度判断
|
||||
else {
|
||||
return element.scrollHeight > element.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标进入处理
|
||||
const handleMouseEnter = (event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement
|
||||
console.log(checkOverflow(element))
|
||||
isOverflow.value = checkOverflow(element)
|
||||
}
|
||||
|
||||
// 鼠标离开处理
|
||||
const handleMouseLeave = () => {
|
||||
isOverflow.value = false
|
||||
}
|
||||
|
||||
// 初始化时检查溢出状态
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (containerRef.value) {
|
||||
isOverflow.value = checkOverflow(containerRef.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multi-line-ellipsis {
|
||||
cursor: default;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
36
frontend/src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
iconClass: string
|
||||
prefix?: string
|
||||
color?: string
|
||||
size?: string
|
||||
}>(),
|
||||
{
|
||||
prefix: 'icon',
|
||||
color: '',
|
||||
size: '1em',
|
||||
},
|
||||
)
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg aria-hidden="true" class="svg-icon" :style="`color:${props.color}`">
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
display: inline-block;
|
||||
width: v-bind('props.size');
|
||||
height: v-bind('props.size');
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em; /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
|
||||
outline: none;
|
||||
fill: currentcolor; /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
|
||||
}
|
||||
</style>
|
||||
71
frontend/src/layout/components/Header.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { useConfigStore } from '@/stores'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
defineOptions({
|
||||
name: 'AppHeader'
|
||||
})
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const isDark = ref(false)
|
||||
|
||||
// 检测当前主题
|
||||
const checkDarkMode = () => {
|
||||
isDark.value =
|
||||
document.documentElement.classList.contains('dark') ||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkDarkMode()
|
||||
|
||||
// 监听系统主题变化
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
mediaQuery.addEventListener('change', checkDarkMode)
|
||||
|
||||
// 监听类名变化
|
||||
const observer = new MutationObserver(checkDarkMode)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
mediaQuery.removeEventListener('change', checkDarkMode)
|
||||
observer.disconnect()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-[var(--color-bg)] h-[60px] relative pl-[28px] font-[900] text-[24px] absolute left-1/2 transform -translate-x-1/2 w-[600px] h-[60px] dark:bg-black flex items-center justify-center transition-[top]"
|
||||
>
|
||||
<svg-icon
|
||||
icon-class="icons"
|
||||
class="header-icon"
|
||||
size="100% "
|
||||
:style="{
|
||||
filter: isDark
|
||||
? 'drop-shadow(0px 0px 5px rgba(0, 0, 0, 0.5))'
|
||||
: 'drop-shadow(0px 0px 5px rgba(161, 161, 161, 0.2))',
|
||||
stroke: 'var(--color-border)',
|
||||
strokeWidth: '1px',
|
||||
strokeLinejoin: 'round'
|
||||
}"
|
||||
/>
|
||||
<h2
|
||||
className="flex items-center gap-3 text-xl font-bold text-zinc-700 dark:text-zinc-300 mr-3 absolute left-1/2 transform -translate-x-1/2"
|
||||
>
|
||||
<img src="/logo.png" alt="logo" className="w-8" />
|
||||
<p>
|
||||
<span className="text-[#9e0000]">数联网</span
|
||||
><span className="text-[var(--color-text-title)]">多智能体协同平台</span>
|
||||
</p>
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.header-icon {
|
||||
color: var(--color-header-bg);
|
||||
}
|
||||
</style>
|
||||
427
frontend/src/layout/components/Main/Task.vue
Normal file
@@ -0,0 +1,427 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed, reactive } from 'vue'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { useAgentsStore, useConfigStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import { changeBriefs } from '@/utils/collaboration_Brief_FrontEnd.ts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { log } from '@jsplumb/browser-ui'
|
||||
import ProcessCard from './TaskTemplate/TaskProcess/ProcessCard.vue'
|
||||
import AgentAllocation from './TaskTemplate/TaskSyllabus/components/AgentAllocation.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search-start'): void
|
||||
(e: 'search', value: string): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const configStore = useConfigStore()
|
||||
const searchValue = ref('')
|
||||
const triggerOnFocus = ref(true)
|
||||
const isFocus = ref(false)
|
||||
const hasAutoSearched = ref(false) // 防止重复自动搜索
|
||||
const agentAllocationVisible = ref(false) // 智能体分配弹窗是否可见
|
||||
const isExpanded = ref(false) // 控制搜索框是否展开
|
||||
|
||||
// 解析URL参数
|
||||
function getUrlParam(param: string): string | null {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
return urlParams.get(param)
|
||||
}
|
||||
|
||||
// 自动搜索函数
|
||||
async function autoSearchFromUrl() {
|
||||
const query = getUrlParam('q')
|
||||
if (query && !hasAutoSearched.value) {
|
||||
// 解码URL参数
|
||||
const decodedQuery = decodeURIComponent(query)
|
||||
searchValue.value = decodedQuery
|
||||
hasAutoSearched.value = true
|
||||
|
||||
// 延迟执行搜索,确保组件已完全渲染
|
||||
setTimeout(() => {
|
||||
handleSearch()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 智能体分配
|
||||
// 处理智能体分配点击事件
|
||||
function handleAgentAllocation() {
|
||||
agentAllocationVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭智能体分配弹窗
|
||||
function handleAgentAllocationClose() {
|
||||
agentAllocationVisible.value = false
|
||||
}
|
||||
|
||||
// 处理获取焦点事件
|
||||
function handleFocus() {
|
||||
isFocus.value = true
|
||||
isExpanded.value = true // 搜索框展开
|
||||
}
|
||||
|
||||
// 处理失去焦点事件
|
||||
function handleBlur() {
|
||||
isFocus.value = false
|
||||
// 延迟收起搜索框,以便点击按钮等操作
|
||||
setTimeout(() => {
|
||||
isExpanded.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
async function handleSearch() {
|
||||
try {
|
||||
triggerOnFocus.value = false
|
||||
if (!searchValue.value) {
|
||||
ElMessage.warning('请输入搜索内容')
|
||||
return
|
||||
}
|
||||
emit('search-start')
|
||||
agentsStore.resetAgent()
|
||||
agentsStore.setAgentRawPlan({ loading: true })
|
||||
const data = await api.generateBasePlan({
|
||||
goal: searchValue.value,
|
||||
inputs: []
|
||||
})
|
||||
data['Collaboration Process'] = changeBriefs(data['Collaboration Process'])
|
||||
agentsStore.setAgentRawPlan({ data })
|
||||
emit('search', searchValue.value)
|
||||
} finally {
|
||||
triggerOnFocus.value = true
|
||||
agentsStore.setAgentRawPlan({ loading: false })
|
||||
}
|
||||
}
|
||||
|
||||
const querySearch = (queryString: string, cb: (v: { value: string }[]) => void) => {
|
||||
const results = queryString
|
||||
? configStore.config.taskPromptWords.filter(createFilter(queryString))
|
||||
: configStore.config.taskPromptWords
|
||||
// call callback function to return suggestions
|
||||
cb(results.map(item => ({ value: item })))
|
||||
}
|
||||
|
||||
const createFilter = (queryString: string) => {
|
||||
return (restaurant: string) => {
|
||||
return restaurant.toLowerCase().includes(queryString.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
const taskContainerRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
// 组件挂载时检查URL参数
|
||||
onMounted(() => {
|
||||
autoSearchFromUrl()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tooltip
|
||||
content="请先点击智能体库右侧的按钮上传智能体信息"
|
||||
placement="top"
|
||||
effect="light"
|
||||
:disabled="agentsStore.agents.length > 0"
|
||||
>
|
||||
<div class="task-root-container">
|
||||
<div
|
||||
class="task-container"
|
||||
ref="taskContainerRef"
|
||||
id="task-container"
|
||||
:class="{ expanded: isExpanded }"
|
||||
>
|
||||
<span class="text-[var(--color-text-task)] font-bold task-title">任务</span>
|
||||
<el-autocomplete
|
||||
v-model.trim="searchValue"
|
||||
class="task-input"
|
||||
size="large"
|
||||
:rows="isFocus ? 3 : 1"
|
||||
placeholder="请输入您的任务"
|
||||
type="textarea"
|
||||
:append-to="taskContainerRef"
|
||||
:fetch-suggestions="querySearch"
|
||||
@change="agentsStore.setSearchValue"
|
||||
:disabled="!(agentsStore.agents.length > 0)"
|
||||
:debounce="0"
|
||||
:trigger-on-focus="triggerOnFocus"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@select="isFocus = false"
|
||||
>
|
||||
</el-autocomplete>
|
||||
<el-button
|
||||
class="task-button"
|
||||
color="linear-gradient(to right, #00C7D2, #315AB4)"
|
||||
size="large"
|
||||
title="点击搜索任务"
|
||||
circle
|
||||
:loading="agentsStore.agentRawPlan.loading"
|
||||
:disabled="!searchValue"
|
||||
@click.stop="handleSearch"
|
||||
>
|
||||
<SvgIcon
|
||||
v-if="!agentsStore.agentRawPlan.loading"
|
||||
icon-class="paper-plane"
|
||||
size="18px"
|
||||
color="#ffffff"
|
||||
/>
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 智能体分配 -->
|
||||
<!-- <div class="agent-allocation-entry" @click="handleAgentAllocation" title="智能体分配">
|
||||
<SvgIcon icon-class="agent-change" size="20px" color="var(--color-bg)" />
|
||||
</div> -->
|
||||
|
||||
<!-- 智能体分配弹窗 -->
|
||||
<el-dialog
|
||||
v-model="agentAllocationVisible"
|
||||
width="70%"
|
||||
top="10vh"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:show-close="false"
|
||||
custom-class="agent-allocation-dialog"
|
||||
>
|
||||
<AgentAllocation @close="handleAgentAllocationClose" />
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-root-container {
|
||||
height: 60px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
}
|
||||
.task-container {
|
||||
width: 40%;
|
||||
margin: 0 auto;
|
||||
border: 2px solid transparent;
|
||||
$bg: var(--el-input-bg-color, var(--el-fill-color-blank));
|
||||
background: linear-gradient(var(--color-bg-taskbar), var(--color-bg-taskbar)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
border-radius: 30px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 998;
|
||||
min-height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0 55px 0 47px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
/* 搜索框展开时的样式 */
|
||||
&.expanded {
|
||||
box-shadow: var(--color-task-shadow);
|
||||
}
|
||||
|
||||
:deep(.el-popper) {
|
||||
position: static !important;
|
||||
width: calc(100% + 102px); /*增加左右padding的总和 */
|
||||
min-width: calc(100% + 102px); /* 确保最小宽度也增加 */
|
||||
margin-left: -47px; /* 向左偏移左padding的值 */
|
||||
margin-right: -55px; /*向右偏移右padding的值 */
|
||||
background: var(--color-bg-taskbar);
|
||||
border: none;
|
||||
transition: height 0s ease-in-out;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-radius: 0;
|
||||
|
||||
li {
|
||||
height: 45px;
|
||||
box-sizing: border-box;
|
||||
line-height: 45px;
|
||||
font-size: 14px;
|
||||
padding-left: 27px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-hover);
|
||||
color: var(--color-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper__arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-autocomplete) {
|
||||
min-height: 56px;
|
||||
width: 100%;
|
||||
|
||||
.task-input {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
font-size: 14px;
|
||||
height: 100%;
|
||||
line-height: 1.5;
|
||||
padding: 18px 0 0 18px;
|
||||
resize: none;
|
||||
color: var(--color-text-taskbar);
|
||||
|
||||
&::placeholder {
|
||||
line-height: 1.2;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.el-icon.is-loading {
|
||||
& + span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-title {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 27px;
|
||||
z-index: 999;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.task-button {
|
||||
background: linear-gradient(to right, #00c7d2, #315ab4);
|
||||
border: none; // 如果需要移除边框
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-button.is-loading {
|
||||
:deep(span) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.process-list {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.process-item {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-list);
|
||||
border: 1px solid var(--color-border-default);
|
||||
|
||||
.process-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
.agent-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-text {
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-container {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.process-item:hover {
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.agent-allocation-entry {
|
||||
position: absolute;
|
||||
right: 0; /* 顶头 */
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: #00aaff; /* 纯蓝色背景 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(0, 170, 255, 0.35);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
// 智能体分配弹窗样式
|
||||
:deep(.agent-allocation-dialog) {
|
||||
.el-dialog {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||
|
||||
&__header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-right: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 0;
|
||||
max-height: 70vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__headerbtn {
|
||||
top: 16px;
|
||||
right: 20px;
|
||||
|
||||
.el-dialog__close {
|
||||
color: var(--color-text);
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { type Agent, useAgentsStore } from '@/stores'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const porps = defineProps<{
|
||||
agentList: Agent[]
|
||||
}>()
|
||||
|
||||
const taskProcess = computed(() => {
|
||||
const list = agentsStore.currentTask?.TaskProcess ?? []
|
||||
return list.map(item => ({
|
||||
...item,
|
||||
key: uuidv4()
|
||||
}))
|
||||
})
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="item in porps.agentList"
|
||||
:key="item.Name"
|
||||
class="user-item"
|
||||
:class="agentsStore.currentTask?.AgentSelection?.includes(item.Name) ? 'active-card' : ''"
|
||||
>
|
||||
<div class="flex items-center justify-between relative h-[43px]">
|
||||
<div
|
||||
class="w-[44px] h-[44px] rounded-full flex items-center justify-center flex-shrink-0 relative right-[2px] icon-container"
|
||||
:style="{ background: getAgentMapIcon(item.Name).color }"
|
||||
>
|
||||
<svg-icon :icon-class="getAgentMapIcon(item.Name).icon" color="#fff" size="24px" />
|
||||
</div>
|
||||
<div class="flex-1 text-[14px] textClass flex flex-col items-end justify-end truncate ml-1">
|
||||
<div class="flex items-center justify-end gap-2 w-full">
|
||||
<span
|
||||
class="truncate"
|
||||
:style="
|
||||
agentsStore.currentTask?.AgentSelection?.includes(item.Name)
|
||||
? 'color:var(--color-accent)'
|
||||
: ''
|
||||
"
|
||||
>{{ item.Name }}</span
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="agentsStore.currentTask?.AgentSelection?.includes(item.Name)"
|
||||
class="flex items-center gap-[7px] h-[8px] mr-1"
|
||||
>
|
||||
<!-- 小圆点 -->
|
||||
<div
|
||||
v-for="item1 in taskProcess.filter(i => i.AgentName === item.Name)"
|
||||
:key="item1.key"
|
||||
class="w-[6px] h-[6px] rounded-full"
|
||||
:style="{
|
||||
background: getActionTypeDisplay(item1.ActionType)?.color,
|
||||
border: `1px solid ${getActionTypeDisplay(item1.ActionType)?.border}`
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 职责信息只有当执行流程中有当前智能体并且鼠标移入时才显示 -->
|
||||
<div class="duty-info">
|
||||
<div class="w-full flex justify-center">
|
||||
<div
|
||||
class="rounded-[9px] bg-[var(--color-bg-quaternary)] text-[12px] py-0.5 px-5 text-center my-2"
|
||||
>
|
||||
当前职责
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-[8px] pt-0">
|
||||
<div
|
||||
v-for="(item1, index1) in taskProcess.filter(i => i.AgentName === item.Name)"
|
||||
:key="item1.key"
|
||||
class="text-[12px]"
|
||||
>
|
||||
<div>
|
||||
<div class="mx-1 inline-block h-[14px]">
|
||||
<div
|
||||
:style="{ background: getActionTypeDisplay(item1.ActionType)?.color }"
|
||||
class="w-[6px] h-[6px] rounded-full mt-[7px]"
|
||||
></div>
|
||||
</div>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }"
|
||||
>{{ getActionTypeDisplay(item1.ActionType)?.name }}:</span
|
||||
>
|
||||
<span>{{ item1.Description }}</span>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div
|
||||
v-if="index1 !== taskProcess.filter(i => i.AgentName === item.Name).length - 1"
|
||||
class="h-[1px] w-full bg-[var(--color-border-default)] my-[8px]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-item {
|
||||
background: var(--color-agent-list-bg);
|
||||
border-radius: 40px;
|
||||
padding-right: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
color: var(--color-text-detail);
|
||||
border: 1px solid var(--color-agent-list-border);
|
||||
box-sizing: border-box;
|
||||
.duty-info {
|
||||
transition: height 0.25s ease;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& + .user-item {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
color: var(--color-text);
|
||||
background: var(--color-agent-list-hover-bg);
|
||||
border: 1px solid var(--color-agent-list-hover-border);
|
||||
}
|
||||
}
|
||||
|
||||
.textClass {
|
||||
color: var(--color-text-agent-list);
|
||||
&:hover {
|
||||
color: var(--color-text-agent-list-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-quaternary), var(--color-bg-quaternary)) padding-box,
|
||||
linear-gradient(to right, var(--color-accent), var(--color-accent-secondary)) border-box;
|
||||
border: 1px solid var(--color-agent-list-border);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
color: var(--color-text);
|
||||
background: var(--color-agent-list-hover-bg);
|
||||
border: 1px solid var(--color-agent-list-hover-border);
|
||||
border-radius: 20px;
|
||||
.duty-info {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加头像容器样式修复
|
||||
.icon-container {
|
||||
right: 0 !important;
|
||||
margin-left: 0px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
import api from '@/api/index.ts'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { agentMapDuty } from '@/layout/components/config.ts'
|
||||
import { type Agent, useAgentsStore } from '@/stores'
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import AgentRepoList from './AgentRepoList.vue'
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 如果agentsStore.agents不存在就读取默认配置的json文件
|
||||
onMounted(async () => {
|
||||
if (!agentsStore.agents.length) {
|
||||
const res = await readConfig<Agent[]>('agent.json')
|
||||
agentsStore.setAgents(res)
|
||||
}
|
||||
await api.setAgents(agentsStore.agents.map(item => pick(item, ['Name', 'Profile'])))
|
||||
})
|
||||
|
||||
// 上传agent文件
|
||||
const fileInput = ref<HTMLInputElement>()
|
||||
|
||||
const triggerFileSelect = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const handleFileSelect = (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files && input.files[0]) {
|
||||
const file = input.files[0]
|
||||
readFileContent(file)
|
||||
}
|
||||
}
|
||||
|
||||
const readFileContent = async (file: File) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async e => {
|
||||
if (!e.target?.result) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(e.target.result?.toString?.() ?? '{}')
|
||||
// 处理 JSON 数据
|
||||
if (Array.isArray(json)) {
|
||||
const isValid = json.every(
|
||||
item =>
|
||||
typeof item.Name === 'string' &&
|
||||
typeof item.Icon === 'string' &&
|
||||
typeof item.Profile === 'string'
|
||||
)
|
||||
if (isValid) {
|
||||
// 处理有效的 JSON 数据
|
||||
agentsStore.setAgents(
|
||||
json.map(item => ({
|
||||
Name: item.Name,
|
||||
Icon: item.Icon.replace(/\.png$/, ''),
|
||||
Profile: item.Profile,
|
||||
Classification: item.Classification
|
||||
}))
|
||||
)
|
||||
await api.setAgents(json.map(item => pick(item, ['Name', 'Profile'])))
|
||||
} else {
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error('JSON is not an array')
|
||||
ElNotification.error({
|
||||
title: '错误',
|
||||
message: 'JSON 格式错误'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// 根据currentTask排序agent列表
|
||||
const agentList = computed(() => {
|
||||
const selected: Agent[] = []
|
||||
const unselected: {
|
||||
title: string
|
||||
data: Agent[]
|
||||
}[] = []
|
||||
const obj: Record<string, Agent[]> = {}
|
||||
if (!agentsStore.agents.length) {
|
||||
return {
|
||||
selected,
|
||||
unselected
|
||||
}
|
||||
}
|
||||
for (const agent of agentsStore.agents) {
|
||||
// if (agentsStore.currentTask?.AgentSelection?.includes(agent.Name)) {
|
||||
// selected.push(agent)
|
||||
// continue
|
||||
// }
|
||||
if (obj[agent.Classification]) {
|
||||
obj[agent.Classification]!.push(agent)
|
||||
} else {
|
||||
const arr = [agent]
|
||||
obj[agent.Classification] = arr
|
||||
unselected.push({
|
||||
title: agent.Classification,
|
||||
data: arr
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
selected,
|
||||
unselected: unselected
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-repo h-full flex flex-col" id="agent-repo">
|
||||
<!-- 头部 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[18px] font-bold text-[var(--color-text-title-header)]">智能体库</span>
|
||||
<!-- 上传文件 -->
|
||||
<input type="file" accept=".json" @change="handleFileSelect" class="hidden" ref="fileInput" />
|
||||
<div class="plus-button" @click="triggerFileSelect">
|
||||
<svg-icon icon-class="plus" color="var(--color-text)" size="18px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 智能体列表 -->
|
||||
<div class="pt-[18px] flex-1 overflow-y-auto relative">
|
||||
<!-- 已选中的智能体 -->
|
||||
<AgentRepoList :agent-list="agentList.selected" />
|
||||
<!-- 为选择的智能体 -->
|
||||
<div v-for="agent in agentList.unselected" :key="agent.title">
|
||||
<p class="text-[12px] font-bold py-[8px]">{{ agent.title }}</p>
|
||||
<AgentRepoList :agent-list="agent.data" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部提示栏 -->
|
||||
<div
|
||||
class="w-full grid grid-cols-3 gap-x-[10px] bg-[var(--color-bg-indicator)] rounded-[20px] p-[8px] mt-[10px]"
|
||||
>
|
||||
<div
|
||||
v-for="item in Object.values(agentMapDuty)"
|
||||
:key="item.key"
|
||||
class="flex items-center justify-center gap-x-1"
|
||||
>
|
||||
<div
|
||||
class="w-[8px] h-[8px] rounded-full"
|
||||
:style="{
|
||||
background: item.color,
|
||||
border: `1px solid ${item.border}`
|
||||
}"
|
||||
></div>
|
||||
<span class="text-[12px]">{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-repo {
|
||||
padding: 0 8px;
|
||||
|
||||
.plus-button {
|
||||
background: var(--color-bg-tertiary);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-quaternary);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#agent-repo {
|
||||
:deep(.agent-repo-item-popover) {
|
||||
padding: 0;
|
||||
border-radius: 20px;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { getActionTypeDisplay } from '@/layout/components/config.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
step: {
|
||||
Id?: string
|
||||
TaskProcess: Array<{
|
||||
ID: string
|
||||
ActionType: string
|
||||
AgentName: string
|
||||
Description: string
|
||||
}>
|
||||
}
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'open-edit', stepId: string, processId: string): void
|
||||
(e: 'save-edit', stepId: string, processId: string, value: string): void
|
||||
}>()
|
||||
|
||||
// 当前正在编辑的process ID
|
||||
const editingProcessId = ref<string | null>(null)
|
||||
// 编辑框的值
|
||||
const editValue = ref('')
|
||||
// 鼠标悬停的process ID
|
||||
const hoverProcessId = ref<string | null>(null)
|
||||
|
||||
// 获取颜色浅两号的函数
|
||||
function getLightColor(color: string, level: number = 2): string {
|
||||
if (!color || color.length !== 7 || color[0] !== '#') return color
|
||||
|
||||
const r = parseInt(color.substr(1, 2), 16)
|
||||
const g = parseInt(color.substr(3, 2), 16)
|
||||
const b = parseInt(color.substr(5, 2), 16)
|
||||
|
||||
// 增加亮度(浅两号)
|
||||
const lightenAmount = level * 20
|
||||
const newR = Math.min(255, r + lightenAmount)
|
||||
const newG = Math.min(255, g + lightenAmount)
|
||||
const newB = Math.min(255, b + lightenAmount)
|
||||
|
||||
return `#${Math.round(newR).toString(16).padStart(2, '0')}${Math.round(newG)
|
||||
.toString(16)
|
||||
.padStart(2, '0')}${Math.round(newB).toString(16).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 处理鼠标进入
|
||||
function handleMouseEnter(processId: string) {
|
||||
hoverProcessId.value = processId
|
||||
}
|
||||
|
||||
// 处理鼠标离开
|
||||
function handleMouseLeave() {
|
||||
hoverProcessId.value = null
|
||||
}
|
||||
|
||||
// 处理双击编辑(针对单个process)
|
||||
function handleDblClick(processId: string, currentDescription: string) {
|
||||
editingProcessId.value = processId
|
||||
editValue.value = currentDescription
|
||||
emit('open-edit', props.step.Id || '', processId)
|
||||
}
|
||||
|
||||
// 处理保存编辑
|
||||
function handleSave(processId: string) {
|
||||
if (!editingProcessId.value) return
|
||||
|
||||
emit('save-edit', props.step.Id || '', processId, editValue.value)
|
||||
editingProcessId.value = null
|
||||
editValue.value = ''
|
||||
}
|
||||
|
||||
// 处理取消编辑
|
||||
function handleCancel() {
|
||||
editingProcessId.value = null
|
||||
editValue.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="process-card">
|
||||
<div class="process-content">
|
||||
<!-- 显示模式 -->
|
||||
<div class="display-content">
|
||||
<span
|
||||
v-for="process in step.TaskProcess"
|
||||
:key="process.ID"
|
||||
class="process-segment"
|
||||
@mouseenter="handleMouseEnter(process.ID)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<span
|
||||
class="agent-name"
|
||||
:style="{
|
||||
backgroundColor: getActionTypeDisplay(process.ActionType)?.color || '#909399',
|
||||
color: '#fff',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
marginRight: '4px'
|
||||
}"
|
||||
>
|
||||
{{ process.AgentName }}
|
||||
</span>
|
||||
|
||||
<!-- 编辑模式 - 修改为卡片样式 -->
|
||||
<div v-if="editingProcessId === process.ID" class="edit-container">
|
||||
<div class="edit-card">
|
||||
<el-input
|
||||
v-model="editValue"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入描述内容"
|
||||
autofocus
|
||||
/>
|
||||
<div class="edit-buttons">
|
||||
<el-button
|
||||
type="success"
|
||||
size="small"
|
||||
icon="Check"
|
||||
@click="handleSave(process.ID)"
|
||||
title="保存"
|
||||
>
|
||||
√
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
icon="Close"
|
||||
@click="handleCancel"
|
||||
title="取消"
|
||||
>
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示模式 -->
|
||||
<span
|
||||
v-else
|
||||
class="process-description"
|
||||
:class="{ hovered: hoverProcessId === process.ID }"
|
||||
:style="{
|
||||
border: `1px solid ${getActionTypeDisplay(process.ActionType)?.border}`,
|
||||
backgroundColor:
|
||||
hoverProcessId === process.ID
|
||||
? getLightColor(getActionTypeDisplay(process.ActionType)?.color || '#909399')
|
||||
: 'transparent'
|
||||
}"
|
||||
@dblclick="handleDblClick(process.ID, process.Description)"
|
||||
>
|
||||
{{ process.Description }}
|
||||
</span>
|
||||
|
||||
<span class="separator" v-if="!process.Description.endsWith('。')">。</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.process-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-list);
|
||||
border: 1px solid var(--color-border-default);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.process-content {
|
||||
min-height: 20px;
|
||||
|
||||
.display-content {
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
|
||||
.process-segment {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
||||
.agent-name {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
margin-right: 4px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-container {
|
||||
display: block; // 改为块级元素,使其换行显示
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.edit-card {
|
||||
position: relative;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
|
||||
:deep(.el-textarea) {
|
||||
width: 100%;
|
||||
|
||||
.el-textarea__inner {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
.el-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
|
||||
&:first-child {
|
||||
background-color: #67c23a;
|
||||
border-color: #67c23a;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
background-color: #f56c6c;
|
||||
border-color: #f56c6c;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-description {
|
||||
display: inline;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&.hovered {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:last-child .separator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-card:hover {
|
||||
border-color: var(--el-border-color);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import type { IExecuteRawResponse } from '@/api'
|
||||
import { computed } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import DOMPurify from 'dompurify'
|
||||
import Iod from './Iod.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
executePlans: IExecuteRawResponse[]
|
||||
nodeId?: string
|
||||
actionId?: string
|
||||
}>()
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
breaks: true
|
||||
})
|
||||
|
||||
function sanitize(str?: string) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
const cleanStr = str.replace(/\\n/g, '\n').replace(/\n\s*\d+\./g, '\n$&')
|
||||
const html = md.render(cleanStr)
|
||||
return html
|
||||
// return DOMPurify.sanitize(html)
|
||||
}
|
||||
|
||||
interface Data {
|
||||
Description: string
|
||||
Content: string
|
||||
LogNodeType: string
|
||||
}
|
||||
const data = computed<Data | null>(() => {
|
||||
for (const result of props.executePlans) {
|
||||
if (result.NodeId === props.nodeId) {
|
||||
// LogNodeType 为 object直接渲染Content
|
||||
if (result.LogNodeType === 'object') {
|
||||
return {
|
||||
Description: props.nodeId,
|
||||
Content: sanitize(result.content),
|
||||
LogNodeType: result.LogNodeType
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.ActionHistory) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const action of result.ActionHistory) {
|
||||
if (action.ID === props.actionId) {
|
||||
return {
|
||||
Description: action.Description,
|
||||
Content: sanitize(action.Action_Result),
|
||||
LogNodeType: result.LogNodeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="data" class="card-item w-full pl-[56px] pr-[41px]">
|
||||
<!-- 分割线 -->
|
||||
<div v-if="data.LogNodeType !== 'object'" class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div
|
||||
v-if="data.Description"
|
||||
class="text-[16px] flex items-center gap-1 text-[var(--color-text-secondary)] mb-1"
|
||||
>
|
||||
{{ data.Description }}
|
||||
<Iod v-if="data.LogNodeType !== 'object'" />
|
||||
</div>
|
||||
<div
|
||||
class="rounded-[8px] p-[15px] text-[14px] bg-[var(--color-bg-result-detail)] text-[var(--color-text-detail)]"
|
||||
>
|
||||
<div
|
||||
class="markdown-content max-h-[240px] overflow-y-auto max-w-full"
|
||||
v-html="data.Content"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.markdown-content {
|
||||
:deep(code) {
|
||||
display: block;
|
||||
width: 100px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
interface Iod {
|
||||
name: string
|
||||
data_space: string
|
||||
doId: string
|
||||
fromRepo: string
|
||||
}
|
||||
|
||||
const data = ref<Iod[]>([])
|
||||
const displayIndex = ref(0)
|
||||
|
||||
const displayIod = computed(() => {
|
||||
return data.value[displayIndex.value]!
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await readConfig<{ data: Iod[] }>('iodConfig.json')
|
||||
data.value = res.data
|
||||
})
|
||||
|
||||
function handleNext() {
|
||||
if (displayIndex.value === data.value.length - 1) {
|
||||
displayIndex.value = 0
|
||||
} else {
|
||||
displayIndex.value++
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrev() {
|
||||
if (displayIndex.value === 0) {
|
||||
displayIndex.value = data.value.length - 1
|
||||
} else {
|
||||
displayIndex.value--
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
trigger="hover"
|
||||
width="440"
|
||||
popper-style="background-color: var(--color-bg-result); border: none;"
|
||||
>
|
||||
<template #reference>
|
||||
<div
|
||||
class="rounded-full w-[20px] h-[20px] bg-[var(--color-bg-quaternary)] flex justify-center items-center cursor-pointer"
|
||||
>
|
||||
{{ data.length }}
|
||||
</div>
|
||||
</template>
|
||||
<template #default v-if="data.length">
|
||||
<div class="bg-[var(--color-bg-result)]">
|
||||
<div class="flex justify-between items-center p-2 pb-0 rounded-[8px] text-[16px] font-bold">
|
||||
<span>数联网搜索结果</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- <div>{{ `${displayIndex + 1}/${data.length}` }}</div>
|
||||
<el-button type="primary" size="small" @click="handleNext">下一个</el-button> -->
|
||||
<!-- 关闭 -->
|
||||
<SvgIcon icon-class="close" size="15px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分割线 -->
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div class="p-2 pt-0">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">名称:</div>
|
||||
<div class="text-[var(--color-text-detail)] flex-1 break-words">
|
||||
{{ displayIod.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">数据空间:</div>
|
||||
<div class="text-[var(--color-text-detail)] lex-1 break-words">
|
||||
{{ displayIod.data_space }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">DOID:</div>
|
||||
<div class="text-[var(--color-text-detail)] lex-1 break-words">
|
||||
{{ displayIod.doId }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<div class="font-bold w-[75px] text-right flex-shrink-0">来源仓库:</div>
|
||||
<div class="text-[var(--color-text-detail)] flex-1 break-words break-al">
|
||||
{{ displayIod.fromRepo }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="card-item w-[80px] h-[25px] flex justify-between items-center rounded-[25px] bg-[#b1b1b1] ml-auto px-2"
|
||||
>
|
||||
<div class="text-[14px] text-[#ffffff] font-medium">
|
||||
{{ `${displayIndex + 1}/${data.length}` }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<svg-icon
|
||||
icon-class="left"
|
||||
size="15px"
|
||||
@click="handlePrev"
|
||||
class="cursor-pointer hover:opacity-70"
|
||||
></svg-icon>
|
||||
<svg-icon
|
||||
icon-class="right"
|
||||
size="15px"
|
||||
@click="handleNext"
|
||||
class="cursor-pointer hover:opacity-70"
|
||||
></svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -0,0 +1,861 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import variables from '@/styles/variables.module.scss'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
||||
import ExecutePlan from './ExecutePlan.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refreshLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const drawerVisible = ref(false)
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
// 编辑逻辑
|
||||
const editMode = ref(false) //全局编辑开关
|
||||
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
|
||||
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
|
||||
|
||||
function getProcessDescription(stepId: string, processId: string) {
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
return process?.Description || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
Object.keys(editMap).forEach(key => {
|
||||
if (editMap[key]) {
|
||||
const [stepId, processId] = key.split('-')
|
||||
const value = editBuffer[key]
|
||||
// 确保 value 是字符串类型
|
||||
if (value !== undefined && value !== null) {
|
||||
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
|
||||
handleSaveEdit(stepId, processId, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
function handleOpenEdit(stepId: string, processId: string) {
|
||||
if (!editMode.value) return
|
||||
const key = `${stepId}-${processId}`
|
||||
editMap[key] = true
|
||||
editBuffer[key] = getProcessDescription(stepId, processId)
|
||||
}
|
||||
|
||||
function handleSaveEdit(stepId: string, processId: string, value: string) {
|
||||
const key = `${stepId}-${processId}`
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
if (process) {
|
||||
process.Description = value
|
||||
}
|
||||
}
|
||||
editMap[key] = false
|
||||
ElMessage.success('已保存(前端内存)')
|
||||
}
|
||||
const jsplumb = new Jsplumb('task-results-main', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: { curviness: 30, stub: 10 }
|
||||
}
|
||||
})
|
||||
|
||||
// 操作折叠面板时要实时的刷新连线
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
function handleCollapse() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
jsplumb.repaintEverything()
|
||||
emit('refreshLine')
|
||||
}, 1) as ReturnType<typeof setInterval>
|
||||
|
||||
// 默认三秒后已经完全打开
|
||||
const timer1 = setTimeout(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (timer1) {
|
||||
clearInterval(timer1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建内部连线
|
||||
function createInternalLine(id?: string) {
|
||||
const arr: ConnectArg[] = []
|
||||
jsplumb.reset()
|
||||
collaborationProcess.value.forEach(item => {
|
||||
// 创建左侧流程与产出的连线
|
||||
arr.push({
|
||||
sourceId: `task-results-${item.Id}-0`,
|
||||
targetId: `task-results-${item.Id}-1`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left]
|
||||
})
|
||||
collaborationProcess.value.forEach(jitem => {
|
||||
// 创建左侧产出与上一步流程的连线
|
||||
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
|
||||
arr.push({
|
||||
sourceId: `task-results-${jitem.Id}-1`,
|
||||
targetId: `task-results-${item.Id}-0`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left],
|
||||
config: {
|
||||
type: 'output'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 创建右侧任务程序与InputObject字段的连线
|
||||
jitem.TaskProcess.forEach(i => {
|
||||
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-1`
|
||||
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 创建右侧TaskProcess内部连线
|
||||
item.TaskProcess?.forEach(i => {
|
||||
if (!i.ImportantInput?.length) {
|
||||
return
|
||||
}
|
||||
item.TaskProcess?.forEach(i2 => {
|
||||
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
|
||||
const targetId = `task-results-${item.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
jsplumb.connects(arr)
|
||||
jsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 额外产物编辑状态
|
||||
const editingOutputId = ref<string | null>(null)
|
||||
const editingOutputContent = ref('')
|
||||
|
||||
// 额外产物内容存储
|
||||
const additionalOutputContents = ref<Record<string, string>>({})
|
||||
|
||||
async function handleRun() {
|
||||
try {
|
||||
loading.value = true
|
||||
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
|
||||
agentsStore.setExecutePlan(d)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务过程
|
||||
async function handleTaskProcess() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
// 开始编辑额外产物内容
|
||||
function startOutputEditing(output: string) {
|
||||
editingOutputId.value = output
|
||||
editingOutputContent.value = getAdditionalOutputContent(output) || ''
|
||||
}
|
||||
|
||||
// 保存额外产物内容
|
||||
function saveOutputEditing() {
|
||||
if (editingOutputId.value && editingOutputContent.value.trim()) {
|
||||
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
|
||||
}
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 取消编辑额外产物内容
|
||||
function cancelOutputEditing() {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 获取额外产物内容
|
||||
function getAdditionalOutputContent(output: string) {
|
||||
return additionalOutputContents.value[output] || ''
|
||||
}
|
||||
|
||||
// 处理额外产物的键盘事件
|
||||
function handleOutputKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveOutputEditing()
|
||||
} else if (event.key === 'Escape') {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 添加滚动状态标识
|
||||
const isScrolling = ref(false)
|
||||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 修改滚动处理函数
|
||||
function handleScroll() {
|
||||
isScrolling.value = true
|
||||
emit('refreshLine')
|
||||
// 清除之前的定时器
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer)
|
||||
}
|
||||
jsplumb.repaintEverything()
|
||||
|
||||
// 设置滚动结束检测
|
||||
scrollTimer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 300) as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
// 修改鼠标事件处理函数
|
||||
const handleMouseEnter = throttle(id => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine(id)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const handleMouseLeave = throttle(() => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
function clear() {
|
||||
jsplumb.reset()
|
||||
}
|
||||
|
||||
// ========== 按钮交互状态管理 ==========
|
||||
const buttonHoverState = ref(null) // null | 'process' | 'execute'
|
||||
|
||||
const handleProcessMouseEnter = () => {
|
||||
buttonHoverState.value = 'process'
|
||||
}
|
||||
|
||||
const handleExecuteMouseEnter = () => {
|
||||
if (agentsStore.agentRawPlan.data) {
|
||||
buttonHoverState.value = 'execute'
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtonMouseLeave = () => {
|
||||
setTimeout(() => {
|
||||
buttonHoverState.value = null
|
||||
}, 150)
|
||||
}
|
||||
|
||||
// 计算按钮类名
|
||||
const processBtnClass = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
const executeBtnClass = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮变圆形
|
||||
if (buttonHoverState.value === 'process') {
|
||||
return 'circle'
|
||||
}
|
||||
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
|
||||
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
// 计算按钮是否显示文字
|
||||
const showProcessText = computed(() => {
|
||||
return buttonHoverState.value === 'process'
|
||||
})
|
||||
|
||||
const showExecuteText = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
|
||||
if (buttonHoverState.value === 'process') return false
|
||||
// 其他情况:如果有任务数据就显示文字,否则不显示
|
||||
return agentsStore.agentRawPlan.data
|
||||
})
|
||||
|
||||
// 计算按钮标题
|
||||
const processBtnTitle = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
|
||||
})
|
||||
|
||||
const executeBtnTitle = computed(() => {
|
||||
return showExecuteText.value ? '任务执行' : '点击运行'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
createInternalLine,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full flex flex-col relative"
|
||||
id="task-results"
|
||||
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
|
||||
>
|
||||
<!-- 标题与执行按钮 -->
|
||||
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
|
||||
<span class="text-[var(--color-text-title-header)]">执行结果</span>
|
||||
<div
|
||||
class="flex items-center gap-[14px] task-button-group"
|
||||
@mouseleave="handleButtonMouseLeave"
|
||||
>
|
||||
<!-- 任务过程按钮 -->
|
||||
<el-button
|
||||
:class="processBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="processBtnTitle"
|
||||
@mouseenter="handleProcessMouseEnter"
|
||||
@click="handleTaskProcess"
|
||||
>
|
||||
<svg-icon icon-class="process" />
|
||||
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
||||
</el-button>
|
||||
|
||||
<!-- 任务执行按钮 -->
|
||||
<el-popover
|
||||
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
||||
title="请先输入要执行的任务"
|
||||
:visible="showPopover"
|
||||
@hide="showPopover = false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
:class="executeBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="executeBtnTitle"
|
||||
:disabled="!agentsStore.agentRawPlan.data"
|
||||
@mouseenter="handleExecuteMouseEnter"
|
||||
@click="handleRun"
|
||||
>
|
||||
<svg-icon icon-class="action" />
|
||||
<span v-if="showExecuteText" class="btn-text">任务执行</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="任务过程"
|
||||
direction="rtl"
|
||||
size="30%"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<!-- 头部工具栏 -->
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="title">任务过程</span>
|
||||
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
|
||||
<el-button v-else text icon="Check" @click="save" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="calc(100vh - 120px)">
|
||||
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
|
||||
<div v-else class="process-list">
|
||||
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
||||
<ProcessCard
|
||||
v-for="step in collaborationProcess"
|
||||
:key="step.Id"
|
||||
:step="step"
|
||||
@open-edit="handleOpenEdit"
|
||||
@save-edit="handleSaveEdit"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 overflow-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div id="task-results-main" class="px-[40px] relative">
|
||||
<!-- 原有的流程和产物 -->
|
||||
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:shadow="true"
|
||||
:id="`task-results-${item.Id}-0`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
|
||||
<!-- 折叠面板 -->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
v-for="item1 in item.TaskProcess"
|
||||
:key="`task-results-${item.Id}-${item1.ID}`"
|
||||
:name="`task-results-${item.Id}-${item1.ID}`"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
||||
<div
|
||||
class="flex items-center gap-[15px] rounded-[20px]"
|
||||
:class="{
|
||||
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
|
||||
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
|
||||
}"
|
||||
>
|
||||
<!-- 右侧链接点 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-results-${item.Id}-0-${item1.ID}`"
|
||||
></div>
|
||||
<div
|
||||
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(item1.AgentName).icon"
|
||||
color="#fff"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[16px]">
|
||||
<span>{{ item1.AgentName }}: </span>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
|
||||
{{ getActionTypeDisplay(item1.ActionType)?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:action-id="item1.ID"
|
||||
:node-id="item.StepName"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card"
|
||||
:shadow="true"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:id="`task-results-${item.Id}-1`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
class="output-object"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<div class="text-[18px]">{{ item.OutputObject }}</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:node-id="item.OutputObject"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<div
|
||||
v-for="(output, index) in agentsStore.additionalOutputs"
|
||||
:key="`additional-output-${index}`"
|
||||
class="card-item"
|
||||
>
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<div class="w-full"></div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card additional-output-card"
|
||||
:shadow="false"
|
||||
:id="`additional-output-results-${index}`"
|
||||
>
|
||||
<!-- 产物名称行 -->
|
||||
<div class="text-[18px] mb-3">
|
||||
{{ output }}
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域行 -->
|
||||
<div class="additional-output-editor">
|
||||
<div v-if="editingOutputId === output" class="w-full">
|
||||
<!-- 编辑状态:输入框 + 按钮 -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<el-input
|
||||
v-model="editingOutputContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入产物内容"
|
||||
@keydown="handleOutputKeydown"
|
||||
class="output-editor"
|
||||
size="small"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
|
||||
√
|
||||
</el-button>
|
||||
<el-button @click="cancelOutputEditing" size="small" class="px-3">
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<!-- 非编辑状态:折叠区域 + 编辑按钮 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
|
||||
>
|
||||
<div
|
||||
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
|
||||
>
|
||||
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
|
||||
</div>
|
||||
<el-button
|
||||
@click="startOutputEditing(output)"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<svg-icon icon-class="action" size="12px" />
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#task-results.is-running {
|
||||
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
|
||||
}
|
||||
#task-results {
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
.el-collapse-item + .el-collapse-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list-run);
|
||||
min-height: 41px;
|
||||
line-height: 41px;
|
||||
border-radius: 20px;
|
||||
transition: border-radius 1ms;
|
||||
position: relative;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: var(--color-bg-detail-list);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
background: var(--color-bg-icon-rotate);
|
||||
border-radius: 50px;
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.output-object {
|
||||
.el-collapse-item__header {
|
||||
background: none;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: none;
|
||||
|
||||
.card-item {
|
||||
background: var(--color-bg-detail);
|
||||
padding: 25px;
|
||||
padding-top: 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
.el-card__body {
|
||||
padding-right: 40px;
|
||||
background-color: var(--color-bg-detail);
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-result-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.output-object-card {
|
||||
:deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.additional-output-card {
|
||||
border: 1px dashed #dcdfe6;
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 编辑区域样式调整
|
||||
.el-collapse {
|
||||
border: none;
|
||||
|
||||
.el-collapse-item {
|
||||
.el-collapse-item__header {
|
||||
background: var(--color-bg-detail);
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 额外产物编辑区域样式
|
||||
.additional-output-editor {
|
||||
.output-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-detail);
|
||||
border: 1px solid #dcdfe6;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.output-content-display {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.5;
|
||||
min-height: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑按钮样式
|
||||
.el-button {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新增:按钮交互样式 ==========
|
||||
.task-button-group {
|
||||
.el-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap !important;
|
||||
border: none !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
position: relative;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
&:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
|
||||
&:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形状态
|
||||
.circle {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-width: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
|
||||
.btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆形状态
|
||||
.ellipse {
|
||||
height: 40px !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 0 16px !important;
|
||||
gap: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: inline-block !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,894 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { throttle } from 'lodash'
|
||||
import { AnchorLocations, BezierConnector } from '@jsplumb/browser-ui'
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getActionTypeDisplay, getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import variables from '@/styles/variables.module.scss'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import api from '@/api'
|
||||
import ProcessCard from '../TaskProcess/ProcessCard.vue'
|
||||
import ExecutePlan from './ExecutePlan.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'refreshLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
const drawerVisible = ref(false)
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
// 编辑逻辑
|
||||
const editMode = ref(false) //全局编辑开关
|
||||
const editMap = reactive<Record<string, boolean>>({}) //行级编辑状态
|
||||
const editBuffer = reactive<Record<string, string | undefined>>({}) //临时输入
|
||||
|
||||
function getProcessDescription(stepId: string, processId: string) {
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
return process?.Description || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function save() {
|
||||
Object.keys(editMap).forEach(key => {
|
||||
if (editMap[key]) {
|
||||
const [stepId, processId] = key.split('-')
|
||||
const value = editBuffer[key]
|
||||
// 确保 value 是字符串类型
|
||||
if (value !== undefined && value !== null) {
|
||||
// @ts-ignore - TypeScript 无法正确推断类型,但运行时是安全的
|
||||
handleSaveEdit(stepId, processId, value)
|
||||
}
|
||||
}
|
||||
})
|
||||
editMode.value = false
|
||||
}
|
||||
|
||||
function handleOpenEdit(stepId: string, processId: string) {
|
||||
if (!editMode.value) return
|
||||
const key = `${stepId}-${processId}`
|
||||
editMap[key] = true
|
||||
editBuffer[key] = getProcessDescription(stepId, processId)
|
||||
}
|
||||
|
||||
function handleSaveEdit(stepId: string, processId: string, value: string) {
|
||||
const key = `${stepId}-${processId}`
|
||||
const step = collaborationProcess.value.find(s => s.Id === stepId)
|
||||
if (step) {
|
||||
const process = step.TaskProcess.find(p => p.ID === processId)
|
||||
if (process) {
|
||||
process.Description = value
|
||||
}
|
||||
}
|
||||
editMap[key] = false
|
||||
ElMessage.success('已保存(前端内存)')
|
||||
}
|
||||
const jsplumb = new Jsplumb('task-results-main', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: { curviness: 30, stub: 10 }
|
||||
}
|
||||
})
|
||||
|
||||
// 操作折叠面板时要实时的刷新连线
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
function handleCollapse() {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
jsplumb.repaintEverything()
|
||||
emit('refreshLine')
|
||||
}, 1) as ReturnType<typeof setInterval>
|
||||
|
||||
// 默认三秒后已经完全打开
|
||||
const timer1 = setTimeout(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
}, 3000)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (timer1) {
|
||||
clearInterval(timer1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 创建内部连线
|
||||
function createInternalLine(id?: string) {
|
||||
const arr: ConnectArg[] = []
|
||||
jsplumb.reset()
|
||||
collaborationProcess.value.forEach(item => {
|
||||
// 创建左侧流程与产出的连线
|
||||
arr.push({
|
||||
sourceId: `task-results-${item.Id}-0`,
|
||||
targetId: `task-results-${item.Id}-1`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left]
|
||||
})
|
||||
collaborationProcess.value.forEach(jitem => {
|
||||
// 创建左侧产出与上一步流程的连线
|
||||
if (item.InputObject_List!.includes(jitem.OutputObject ?? '')) {
|
||||
arr.push({
|
||||
sourceId: `task-results-${jitem.Id}-1`,
|
||||
targetId: `task-results-${item.Id}-0`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Left],
|
||||
config: {
|
||||
type: 'output'
|
||||
}
|
||||
})
|
||||
}
|
||||
// 创建右侧任务程序与InputObject字段的连线
|
||||
jitem.TaskProcess.forEach(i => {
|
||||
if (i.ImportantInput?.includes(`InputObject:${item.OutputObject}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-1`
|
||||
const targetId = `task-results-${jitem.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 创建右侧TaskProcess内部连线
|
||||
item.TaskProcess?.forEach(i => {
|
||||
if (!i.ImportantInput?.length) {
|
||||
return
|
||||
}
|
||||
item.TaskProcess?.forEach(i2 => {
|
||||
if (i.ImportantInput.includes(`ActionResult:${i2.ID}`)) {
|
||||
const color = getActionTypeDisplay(i.ActionType)?.color ?? ''
|
||||
const sourceId = `task-results-${item.Id}-0-${i2.ID}`
|
||||
const targetId = `task-results-${item.Id}-0-${i.ID}`
|
||||
arr.push({
|
||||
sourceId,
|
||||
targetId,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Right],
|
||||
config: {
|
||||
stops: [
|
||||
[0, color],
|
||||
[1, color]
|
||||
],
|
||||
transparent: targetId !== id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
jsplumb.connects(arr)
|
||||
jsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 额外产物编辑状态
|
||||
const editingOutputId = ref<string | null>(null)
|
||||
const editingOutputContent = ref('')
|
||||
|
||||
// 额外产物内容存储
|
||||
const additionalOutputContents = ref<Record<string, string>>({})
|
||||
|
||||
async function handleRun() {
|
||||
try {
|
||||
loading.value = true
|
||||
const d = await api.executePlan(agentsStore.agentRawPlan.data!)
|
||||
agentsStore.setExecutePlan(d)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看任务过程
|
||||
async function handleTaskProcess() {
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
// 开始编辑额外产物内容
|
||||
function startOutputEditing(output: string) {
|
||||
editingOutputId.value = output
|
||||
editingOutputContent.value = getAdditionalOutputContent(output) || ''
|
||||
}
|
||||
|
||||
// 保存额外产物内容
|
||||
function saveOutputEditing() {
|
||||
if (editingOutputId.value && editingOutputContent.value.trim()) {
|
||||
additionalOutputContents.value[editingOutputId.value] = editingOutputContent.value.trim()
|
||||
}
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 取消编辑额外产物内容
|
||||
function cancelOutputEditing() {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
|
||||
// 获取额外产物内容
|
||||
function getAdditionalOutputContent(output: string) {
|
||||
return additionalOutputContents.value[output] || ''
|
||||
}
|
||||
|
||||
// 处理额外产物的键盘事件
|
||||
function handleOutputKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveOutputEditing()
|
||||
} else if (event.key === 'Escape') {
|
||||
editingOutputId.value = null
|
||||
editingOutputContent.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 添加滚动状态标识
|
||||
const isScrolling = ref(false)
|
||||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// 修改滚动处理函数
|
||||
function handleScroll() {
|
||||
isScrolling.value = true
|
||||
emit('refreshLine')
|
||||
// 清除之前的定时器
|
||||
if (scrollTimer) {
|
||||
clearTimeout(scrollTimer)
|
||||
}
|
||||
jsplumb.repaintEverything()
|
||||
|
||||
// 设置滚动结束检测
|
||||
scrollTimer = setTimeout(() => {
|
||||
isScrolling.value = false
|
||||
}, 300) as ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
// 修改鼠标事件处理函数
|
||||
const handleMouseEnter = throttle(id => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine(id)
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const handleMouseLeave = throttle(() => {
|
||||
if (!isScrolling.value) {
|
||||
createInternalLine()
|
||||
}
|
||||
}, 100)
|
||||
|
||||
function clear() {
|
||||
jsplumb.reset()
|
||||
}
|
||||
|
||||
// ========== 按钮交互状态管理 ==========
|
||||
const buttonHoverState = ref<'process' | 'execute' | null>(null)
|
||||
let buttonHoverTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleProcessMouseEnter = () => {
|
||||
if (buttonHoverTimer) {
|
||||
clearTimeout(buttonHoverTimer)
|
||||
buttonHoverTimer = null
|
||||
}
|
||||
buttonHoverState.value = 'process'
|
||||
}
|
||||
|
||||
const handleExecuteMouseEnter = () => {
|
||||
if (buttonHoverTimer) {
|
||||
clearTimeout(buttonHoverTimer)
|
||||
buttonHoverTimer = null
|
||||
}
|
||||
if (agentsStore.agentRawPlan.data) {
|
||||
buttonHoverState.value = 'execute'
|
||||
}
|
||||
}
|
||||
|
||||
const handleButtonMouseLeave = () => {
|
||||
// 添加防抖,防止快速切换时的抖动
|
||||
if (buttonHoverTimer) {
|
||||
clearTimeout(buttonHoverTimer)
|
||||
}
|
||||
buttonHoverTimer = setTimeout(() => {
|
||||
buttonHoverState.value = null
|
||||
}, 50) // 适当减少延迟时间
|
||||
}
|
||||
|
||||
// 添加离开组件时的清理
|
||||
onUnmounted(() => {
|
||||
if (buttonHoverTimer) {
|
||||
clearTimeout(buttonHoverTimer)
|
||||
}
|
||||
})
|
||||
// 计算按钮类名
|
||||
const processBtnClass = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
const executeBtnClass = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮变圆形
|
||||
if (buttonHoverState.value === 'process') {
|
||||
return 'circle'
|
||||
}
|
||||
// 其他情况:如果有任务数据就显示椭圆形,否则显示圆形
|
||||
return agentsStore.agentRawPlan.data ? 'ellipse' : 'circle'
|
||||
})
|
||||
|
||||
// 计算按钮是否显示文字
|
||||
const showProcessText = computed(() => {
|
||||
return buttonHoverState.value === 'process'
|
||||
})
|
||||
|
||||
const showExecuteText = computed(() => {
|
||||
// 鼠标悬停在过程按钮上时,执行按钮不显示文字
|
||||
if (buttonHoverState.value === 'process') return false
|
||||
// 其他情况:如果有任务数据就显示文字,否则不显示
|
||||
return agentsStore.agentRawPlan.data
|
||||
})
|
||||
|
||||
// 计算按钮标题
|
||||
const processBtnTitle = computed(() => {
|
||||
return buttonHoverState.value === 'process' ? '任务过程' : '点击查看任务过程'
|
||||
})
|
||||
|
||||
const executeBtnTitle = computed(() => {
|
||||
return showExecuteText.value ? '任务执行' : '点击运行'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
createInternalLine,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full flex flex-col relative"
|
||||
id="task-results"
|
||||
:class="{ 'is-running': agentsStore.executePlan.length > 0 }"
|
||||
>
|
||||
<!-- 标题与执行按钮 -->
|
||||
<div class="text-[18px] font-bold mb-[18px] flex justify-between items-center px-[20px]">
|
||||
<span class="text-[var(--color-text-title-header)]">执行结果</span>
|
||||
<div
|
||||
class="flex items-center gap-[14px] task-button-group"
|
||||
@mouseleave="handleButtonMouseLeave"
|
||||
>
|
||||
<!-- 任务过程按钮 -->
|
||||
<el-button
|
||||
:class="processBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="processBtnTitle"
|
||||
@mouseenter="handleProcessMouseEnter"
|
||||
@click="handleTaskProcess"
|
||||
>
|
||||
<svg-icon icon-class="process" />
|
||||
<span v-if="showProcessText" class="btn-text">任务过程</span>
|
||||
</el-button>
|
||||
|
||||
<!-- 任务执行按钮 -->
|
||||
<el-popover
|
||||
:disabled="Boolean(agentsStore.agentRawPlan.data)"
|
||||
title="请先输入要执行的任务"
|
||||
:visible="showPopover"
|
||||
@hide="showPopover = false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
:class="executeBtnClass"
|
||||
:color="variables.tertiary"
|
||||
:title="executeBtnTitle"
|
||||
:disabled="!agentsStore.agentRawPlan.data"
|
||||
@mouseenter="handleExecuteMouseEnter"
|
||||
@click="handleRun"
|
||||
>
|
||||
<svg-icon icon-class="action" />
|
||||
<span v-if="showExecuteText" class="btn-text">任务执行</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
|
||||
<el-drawer
|
||||
v-model="drawerVisible"
|
||||
title="任务过程"
|
||||
direction="rtl"
|
||||
size="30%"
|
||||
:destroy-on-close="false"
|
||||
>
|
||||
<!-- 头部工具栏 -->
|
||||
<template #header>
|
||||
<div class="drawer-header">
|
||||
<span class="title">任务过程</span>
|
||||
<!-- <el-button v-if="!editMode" text icon="Edit" @click="editMode = true" />
|
||||
<el-button v-else text icon="Check" @click="save" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="calc(100vh - 120px)">
|
||||
<el-empty v-if="!collaborationProcess.length" description="暂无任务过程" />
|
||||
<div v-else class="process-list">
|
||||
<!-- 使用ProcessCard组件显示每个AgentSelection -->
|
||||
<ProcessCard
|
||||
v-for="step in collaborationProcess"
|
||||
:key="step.Id"
|
||||
:step="step"
|
||||
@open-edit="handleOpenEdit"
|
||||
@save-edit="handleSaveEdit"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 overflow-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div id="task-results-main" class="px-[40px] relative">
|
||||
<!-- 原有的流程和产物 -->
|
||||
<div v-for="item in collaborationProcess" :key="item.Id" class="card-item">
|
||||
<el-card
|
||||
class="card-item w-full relative"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:shadow="true"
|
||||
:id="`task-results-${item.Id}-0`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<div class="text-[18px] mb-[15px]">{{ item.StepName }}</div>
|
||||
<!-- 折叠面板 -->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
v-for="item1 in item.TaskProcess"
|
||||
:key="`task-results-${item.Id}-${item1.ID}`"
|
||||
:name="`task-results-${item.Id}-${item1.ID}`"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
@mouseenter="() => handleMouseEnter(`task-results-${item.Id}-0-${item1.ID}`)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<!-- 运行之前背景颜色是var(--color-bg-detail-list),运行之后背景颜色是var(--color-bg-detail-list-run) -->
|
||||
<div
|
||||
class="flex items-center gap-[15px] rounded-[20px]"
|
||||
:class="{
|
||||
'bg-[var(--color-bg-detail-list)]': !agentsStore.executePlan.length,
|
||||
'bg-[var(--color-bg-detail-list-run)]': agentsStore.executePlan.length
|
||||
}"
|
||||
>
|
||||
<!-- 右侧链接点 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-results-${item.Id}-0-${item1.ID}`"
|
||||
></div>
|
||||
<div
|
||||
class="w-[41px] h-[41px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(item1.AgentName).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(item1.AgentName).icon"
|
||||
color="#fff"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[16px]">
|
||||
<span
|
||||
:class="{
|
||||
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
|
||||
'text-[var(--color-text-result-detail-run)]':
|
||||
agentsStore.executePlan.length
|
||||
}"
|
||||
>{{ item1.AgentName }}: </span
|
||||
>
|
||||
<span :style="{ color: getActionTypeDisplay(item1.ActionType)?.color }">
|
||||
{{ getActionTypeDisplay(item1.ActionType)?.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:action-id="item1.ID"
|
||||
:node-id="item.StepName"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card"
|
||||
:shadow="true"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:id="`task-results-${item.Id}-1`"
|
||||
@click="emit('setCurrentTask', item)"
|
||||
>
|
||||
<!-- <div class="text-[18px]">{{ item.OutputObject }}</div>-->
|
||||
<el-collapse @change="handleCollapse">
|
||||
<el-collapse-item
|
||||
class="output-object"
|
||||
:disabled="Boolean(!agentsStore.executePlan.length || loading)"
|
||||
>
|
||||
<template v-if="loading" #icon>
|
||||
<SvgIcon icon-class="loading" size="20px" class="animate-spin" />
|
||||
</template>
|
||||
<template v-else-if="!agentsStore.executePlan.length" #icon>
|
||||
<span></span>
|
||||
</template>
|
||||
<template #title>
|
||||
<div
|
||||
class="text-[18px]"
|
||||
:class="{
|
||||
'text-[var(--color-text-result-detail)]': !agentsStore.executePlan.length,
|
||||
'text-[var(--color-text-result-detail-run)]': agentsStore.executePlan.length
|
||||
}"
|
||||
>
|
||||
{{ item.OutputObject }}
|
||||
</div>
|
||||
</template>
|
||||
<ExecutePlan
|
||||
:node-id="item.OutputObject"
|
||||
:execute-plans="agentsStore.executePlan"
|
||||
/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<div
|
||||
v-for="(output, index) in agentsStore.additionalOutputs"
|
||||
:key="`additional-output-${index}`"
|
||||
class="card-item"
|
||||
>
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<div class="w-full"></div>
|
||||
|
||||
<!-- 额外产物的编辑卡片 -->
|
||||
<el-card
|
||||
class="card-item w-full relative output-object-card additional-output-card"
|
||||
:shadow="false"
|
||||
:id="`additional-output-results-${index}`"
|
||||
>
|
||||
<!-- 产物名称行 -->
|
||||
<div class="text-[18px] mb-3">
|
||||
{{ output }}
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域行 -->
|
||||
<div class="additional-output-editor">
|
||||
<div v-if="editingOutputId === output" class="w-full">
|
||||
<!-- 编辑状态:输入框 + 按钮 -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<el-input
|
||||
v-model="editingOutputContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
placeholder="请输入产物内容"
|
||||
@keydown="handleOutputKeydown"
|
||||
class="output-editor"
|
||||
size="small"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="saveOutputEditing" type="primary" size="small" class="px-3">
|
||||
√
|
||||
</el-button>
|
||||
<el-button @click="cancelOutputEditing" size="small" class="px-3">
|
||||
×
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<!-- 非编辑状态:折叠区域 + 编辑按钮 -->
|
||||
<div
|
||||
class="flex items-center justify-between p-3 bg-[var(--color-bg-quinary)] rounded-[8px]"
|
||||
>
|
||||
<div
|
||||
class="text-[14px] text-[var(--color-text-secondary)] output-content-display"
|
||||
>
|
||||
{{ getAdditionalOutputContent(output) || '暂无内容,点击编辑' }}
|
||||
</div>
|
||||
<el-button
|
||||
@click="startOutputEditing(output)"
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<svg-icon icon-class="action" size="12px" />
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#task-results.is-running {
|
||||
--color-bg-detail-list: var(--color-bg-detail-list-run); // 直接指向 100 % 版本
|
||||
}
|
||||
#task-results {
|
||||
:deep(.el-collapse) {
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
.el-collapse-item + .el-collapse-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list-run);
|
||||
min-height: 41px;
|
||||
line-height: 41px;
|
||||
border-radius: 20px;
|
||||
transition: border-radius 1ms;
|
||||
position: relative;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: var(--color-bg-detail-list);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
background: var(--color-bg-icon-rotate);
|
||||
border-radius: 50px;
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.output-object {
|
||||
.el-collapse-item__header {
|
||||
background: none;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: none;
|
||||
|
||||
.card-item {
|
||||
background: var(--color-bg-detail);
|
||||
padding: 25px;
|
||||
padding-top: 10px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
border: none;
|
||||
background: var(--color-bg-detail-list);
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card) {
|
||||
.el-card__body {
|
||||
padding-right: 40px;
|
||||
background-color: var(--color-bg-detail);
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-result-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.output-object-card {
|
||||
:deep(.el-card__body) {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.active-card {
|
||||
background: linear-gradient(var(--color-bg-tertiary), var(--color-bg-tertiary)) padding-box,
|
||||
linear-gradient(to right, #00c8d2, #315ab4) border-box;
|
||||
}
|
||||
|
||||
.card-item + .card-item {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.additional-output-card {
|
||||
border: 1px dashed #dcdfe6;
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--color-agent-list-hover-shadow);
|
||||
|
||||
&:hover {
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 编辑区域样式调整
|
||||
.el-collapse {
|
||||
border: none;
|
||||
|
||||
.el-collapse-item {
|
||||
.el-collapse-item__header {
|
||||
background: var(--color-bg-detail);
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-collapse-item__title {
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse-item__wrap {
|
||||
background: var(--color-bg-detail);
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 额外产物编辑区域样式
|
||||
.additional-output-editor {
|
||||
.output-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-detail);
|
||||
border: 1px solid #dcdfe6;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.output-content-display {
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.5;
|
||||
min-height: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-color: #409eff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑按钮样式
|
||||
.el-button {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 新增:按钮交互样式 ==========
|
||||
.task-button-group {
|
||||
.el-button {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
|
||||
overflow: hidden !important;
|
||||
white-space: nowrap !important;
|
||||
border: none !important;
|
||||
color: var(--color-text-primary) !important;
|
||||
position: relative;
|
||||
background-color: var(--color-bg-tertiary);
|
||||
&:hover {
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
filter: brightness(1.1) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
|
||||
&:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
filter: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 圆形状态
|
||||
.circle {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
min-width: 40px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
|
||||
.btn-text {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆形状态
|
||||
.ellipse {
|
||||
height: 40px !important;
|
||||
border-radius: 20px !important;
|
||||
padding: 0 16px !important;
|
||||
gap: 8px;
|
||||
|
||||
.btn-text {
|
||||
display: inline-block !important;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 1;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="absolute inset-0 flex items-start gap-[14%]">
|
||||
<!-- 左侧元素 -->
|
||||
<div class="flex-1 relative h-full flex justify-center">
|
||||
<!-- 背景那一根线 -->
|
||||
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧元素 -->
|
||||
<div class="flex-1 relative h-full flex justify-center">
|
||||
<!-- 背景那一根线 -->
|
||||
<div class="h-full bg-[var(--color-bg-flow)] w-[5px]">
|
||||
<!-- 线底部的小圆球 -->
|
||||
<div
|
||||
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 bg-[var(--color-bg-flow)] w-[15px] h-[15px] rounded-full"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
@@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAgentsStore } from '@/stores/modules/agents'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 获取当前选中任务的智能体列表
|
||||
const currentTaskAgents = computed(() => {
|
||||
return agentsStore.currentTask?.AgentSelection || []
|
||||
})
|
||||
|
||||
// 获取智能体列表
|
||||
const agents = computed(() => agentsStore.agents)
|
||||
|
||||
// 选中状态管理 - 初始选中当前任务的智能体
|
||||
const selectedAgents = ref<string[]>([])
|
||||
|
||||
// 初始化选中当前任务的智能体
|
||||
const initializeSelectedAgents = () => {
|
||||
selectedAgents.value = [...currentTaskAgents.value]
|
||||
}
|
||||
|
||||
// 切换选中状态
|
||||
const toggleSelectAgent = (agentName: string) => {
|
||||
const index = selectedAgents.value.indexOf(agentName)
|
||||
if (index > -1) {
|
||||
// 如果已选中,则取消选中
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
// 如果未选中,则添加到选中列表末尾
|
||||
selectedAgents.value.push(agentName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中
|
||||
const isAgentSelected = (agentName: string) => {
|
||||
return selectedAgents.value.includes(agentName)
|
||||
}
|
||||
|
||||
// 获取排序后的智能体列表 - 选中的智能体排在前面,保持原有顺序
|
||||
const sortedAgents = computed(() => {
|
||||
const selected = agents.value.filter(agent => selectedAgents.value.includes(agent.Name))
|
||||
const unselected = agents.value.filter(agent => !selectedAgents.value.includes(agent.Name))
|
||||
return [...selected, ...unselected]
|
||||
})
|
||||
|
||||
// 颜色等级配置 - 从蓝色到白色的5个等级
|
||||
const colorLevels = [
|
||||
{ level: 5, color: '#1d59dc', textColor: '#FFFFFF' }, // 深蓝色
|
||||
{ level: 4, color: '#4a71c7', textColor: '#FFFFFF' }, // 中蓝色
|
||||
{ level: 3, color: '#6c8ed7', textColor: '#333333' }, // 浅蓝色
|
||||
{ level: 2, color: '#9cb7f0', textColor: '#333333' }, // 更浅蓝
|
||||
{ level: 1, color: '#c8d9fc', textColor: '#333333' } // 接近白色
|
||||
]
|
||||
|
||||
// 为每个智能体分配随机等级(实际应用中应该根据业务逻辑分配)
|
||||
const getAgentLevel = (agentIndex: number) => {
|
||||
// 这里使用简单的算法分配等级,实际应用中应该根据智能体的能力、经验等分配
|
||||
return ((agentIndex % 5) + 1) % 6
|
||||
}
|
||||
|
||||
// 获取对应等级的颜色配置
|
||||
const getLevelConfig = (level: number) => {
|
||||
return colorLevels.find(config => config.level === level) || colorLevels[0]
|
||||
}
|
||||
|
||||
// 组件挂载时初始化选中状态
|
||||
onMounted(() => {
|
||||
initializeSelectedAgents()
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-allocation">
|
||||
<!-- 头部 -->
|
||||
<div class="allocation-header">
|
||||
<span class="header-title">智能体分配</span>
|
||||
<el-button class="close-button" text circle @click="handleClose" title="关闭">
|
||||
<span class="close-icon">❌️</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<el-divider class="header-divider" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="allocation-content">
|
||||
<!-- 智能体整体容器 -->
|
||||
<div class="agents-container selectagent">
|
||||
<div
|
||||
v-for="(agent, index) in agents"
|
||||
:key="agent.Name"
|
||||
class="agent-item"
|
||||
:title="agent.Name"
|
||||
@click="toggleSelectAgent(agent.Name)"
|
||||
>
|
||||
<!-- 头像部分(独立元素) -->
|
||||
<div class="agent-avatar" :style="{ background: getAgentMapIcon(agent.Name).color }">
|
||||
<SvgIcon :icon-class="getAgentMapIcon(agent.Name).icon" size="24px" color="#fff" />
|
||||
</div>
|
||||
<!-- 等级色值部分(独立元素,通过CSS定位跟随头像) -->
|
||||
<div
|
||||
class="color-level"
|
||||
:style="{
|
||||
backgroundColor: getLevelConfig(getAgentLevel(index)).color,
|
||||
color: getLevelConfig(getAgentLevel(index)).textColor
|
||||
}"
|
||||
>
|
||||
{{ getAgentLevel(index) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="agents.length === 0" class="empty-state">
|
||||
<el-empty description="暂无智能体数据" />
|
||||
<div class="empty-tip">请先在智能体库中上传智能体信息</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-allocation {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.allocation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
margin: 0;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.allocation-content {
|
||||
flex: 1;
|
||||
padding: 30px 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start; // 改为靠左对齐
|
||||
gap: 20px;
|
||||
|
||||
.agents-container {
|
||||
display: flex;
|
||||
gap: 0; // 确保容器内元素之间没有间隙
|
||||
justify-content: flex-start; // 靠左对齐
|
||||
align-items: flex-start; // 改为顶部对齐
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 0px; // 确保item之间没有水平间距
|
||||
padding: 0; // 移除内边距
|
||||
|
||||
.agent-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--color-border);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
margin-top: 8px; // 头像和等级卡片之间的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
width: 100%;
|
||||
|
||||
.empty-tip {
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
padding: 20px 16px;
|
||||
gap: 16px;
|
||||
|
||||
.agents-container {
|
||||
.agent-item {
|
||||
margin: 0 6px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
margin-top: 8px; // 保持一致的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
.agents-container {
|
||||
.agent-item {
|
||||
margin: 0 4px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
font-size: 12px;
|
||||
margin-top: 8px; // 保持一致的垂直间距
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,505 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { useAgentsStore } from '@/stores/modules/agents'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 获取当前选中任务的智能体列表
|
||||
const currentTaskAgents = computed(() => {
|
||||
return agentsStore.currentTask?.AgentSelection || []
|
||||
})
|
||||
|
||||
// 获取智能体列表
|
||||
const agents = computed(() => agentsStore.agents)
|
||||
|
||||
// 选中状态管理 - 初始选中当前任务的智能体
|
||||
const selectedAgents = ref<string[]>([])
|
||||
|
||||
// 初始化选中当前任务的智能体
|
||||
const initializeSelectedAgents = () => {
|
||||
selectedAgents.value = [...currentTaskAgents.value]
|
||||
}
|
||||
|
||||
// 切换选中状态
|
||||
const toggleSelectAgent = (agentName: string) => {
|
||||
const index = selectedAgents.value.indexOf(agentName)
|
||||
if (index > -1) {
|
||||
// 如果已选中,则取消选中
|
||||
selectedAgents.value.splice(index, 1)
|
||||
} else {
|
||||
// 如果未选中,则添加到选中列表末尾
|
||||
selectedAgents.value.push(agentName)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否选中
|
||||
const isAgentSelected = (agentName: string) => {
|
||||
return selectedAgents.value.includes(agentName)
|
||||
}
|
||||
|
||||
// 获取排序后的智能体列表 - 选中的智能体排在前面,保持原有顺序
|
||||
const sortedAgents = computed(() => {
|
||||
const selected = agents.value.filter(agent => selectedAgents.value.includes(agent.Name))
|
||||
const unselected = agents.value.filter(agent => !selectedAgents.value.includes(agent.Name))
|
||||
return [...selected, ...unselected]
|
||||
})
|
||||
|
||||
// 颜色等级配置 - 从蓝色到白色的5个等级
|
||||
const colorLevels = [
|
||||
{ level: 5, color: '#1d59dc', textColor: '#FFFFFF' }, // 深蓝色
|
||||
{ level: 4, color: '#4a71c7', textColor: '#FFFFFF' }, // 中蓝色
|
||||
{ level: 3, color: '#6c8ed7', textColor: '#333333' }, // 浅蓝色
|
||||
{ level: 2, color: '#9cb7f0', textColor: '#333333' }, // 更浅蓝
|
||||
{ level: 1, color: '#c8d9fc', textColor: '#333333' } // 接近白色
|
||||
]
|
||||
|
||||
// 为每个智能体分配随机等级(实际应用中应该根据业务逻辑分配)
|
||||
const getAgentLevel = (agentIndex: number) => {
|
||||
// 这里使用简单的算法分配等级,实际应用中应该根据智能体的能力、经验等分配
|
||||
return ((agentIndex % 5) + 1) % 6
|
||||
}
|
||||
|
||||
// 获取对应等级的颜色配置
|
||||
const getLevelConfig = (level: number) => {
|
||||
return colorLevels.find(config => config.level === level) || colorLevels[0]
|
||||
}
|
||||
|
||||
// ========== 新增:椭圆框交互逻辑 ==========
|
||||
const ellipseBoxRef = ref<HTMLElement>()
|
||||
const isEllipseVisible = ref(false)
|
||||
const ellipseStyle = ref({
|
||||
width: '0px',
|
||||
height: '0px',
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
opacity: 0
|
||||
})
|
||||
|
||||
// 椭圆框动画配置
|
||||
const ellipseAnimationConfig = {
|
||||
duration: 300,
|
||||
easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)'
|
||||
}
|
||||
|
||||
// 更新椭圆框位置和大小
|
||||
const updateEllipseBox = () => {
|
||||
if (!ellipseBoxRef.value || selectedAgents.value.length === 0) {
|
||||
isEllipseVisible.value = false
|
||||
ellipseStyle.value.opacity = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有选中头像的位置
|
||||
const selectedAvatars = document.querySelectorAll('.agent-item.selected .agent-avatar')
|
||||
|
||||
if (selectedAvatars.length === 0) {
|
||||
isEllipseVisible.value = false
|
||||
ellipseStyle.value.opacity = 0
|
||||
return
|
||||
}
|
||||
|
||||
// 计算椭圆框的位置和尺寸
|
||||
let minLeft = Infinity
|
||||
let maxRight = -Infinity
|
||||
let minTop = Infinity
|
||||
let maxBottom = -Infinity
|
||||
|
||||
selectedAvatars.forEach(avatar => {
|
||||
const rect = avatar.getBoundingClientRect()
|
||||
const containerRect = ellipseBoxRef.value!.parentElement!.getBoundingClientRect()
|
||||
|
||||
const left = rect.left - containerRect.left
|
||||
const right = rect.right - containerRect.left
|
||||
const top = rect.top - containerRect.top
|
||||
const bottom = rect.bottom - containerRect.top
|
||||
|
||||
minLeft = Math.min(minLeft, left)
|
||||
maxRight = Math.max(maxRight, right)
|
||||
minTop = Math.min(minTop, top)
|
||||
maxBottom = Math.max(maxBottom, bottom)
|
||||
})
|
||||
|
||||
// 计算椭圆框的尺寸(增加一些内边距)
|
||||
const padding = 8
|
||||
const width = maxRight - minLeft + padding * 2
|
||||
const height = maxBottom - minTop + padding * 2
|
||||
|
||||
ellipseStyle.value = {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
left: `${minLeft - padding}px`,
|
||||
top: `${minTop - padding}px`,
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
isEllipseVisible.value = true
|
||||
}
|
||||
|
||||
// 监听选中状态变化,更新椭圆框
|
||||
watch(
|
||||
selectedAgents,
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
updateEllipseBox()
|
||||
}, 50) // 等待DOM更新
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 组件挂载时初始化选中状态
|
||||
onMounted(() => {
|
||||
initializeSelectedAgents()
|
||||
|
||||
// 初始渲染后更新椭圆框
|
||||
setTimeout(() => {
|
||||
updateEllipseBox()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-allocation">
|
||||
<!-- 头部 -->
|
||||
<div class="allocation-header">
|
||||
<span class="header-title">智能体分配</span>
|
||||
<el-button class="close-button" text circle @click="handleClose" title="关闭">
|
||||
<span class="close-icon">❌️</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<el-divider class="header-divider" />
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="allocation-content">
|
||||
<!-- 椭圆框容器 -->
|
||||
<div class="ellipse-box-container">
|
||||
<!-- 椭圆框(动画元素) -->
|
||||
<div
|
||||
ref="ellipseBoxRef"
|
||||
class="ellipse-box"
|
||||
:class="{ visible: isEllipseVisible }"
|
||||
:style="ellipseStyle"
|
||||
/>
|
||||
|
||||
<!-- 智能体整体容器 -->
|
||||
<div class="agents-container">
|
||||
<div
|
||||
v-for="(agent, index) in agents"
|
||||
:key="agent.Name"
|
||||
class="agent-item"
|
||||
:class="{ selected: isAgentSelected(agent.Name) }"
|
||||
:title="agent.Name"
|
||||
@click="toggleSelectAgent(agent.Name)"
|
||||
>
|
||||
<!-- 头像部分(独立元素) -->
|
||||
<div
|
||||
class="agent-avatar"
|
||||
:style="{
|
||||
background: getAgentMapIcon(agent.Name).color,
|
||||
borderColor: isAgentSelected(agent.Name) ? '#409eff' : 'var(--color-border)'
|
||||
}"
|
||||
>
|
||||
<SvgIcon :icon-class="getAgentMapIcon(agent.Name).icon" size="24px" color="#fff" />
|
||||
|
||||
<!-- 选中标记 -->
|
||||
<div v-if="isAgentSelected(agent.Name)" class="selected-indicator">
|
||||
<svg-icon icon-class="check" size="12px" color="#fff" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 等级色值部分(独立元素,通过CSS定位跟随头像) -->
|
||||
<div
|
||||
class="color-level"
|
||||
:style="{
|
||||
backgroundColor: getLevelConfig(getAgentLevel(index)).color,
|
||||
color: getLevelConfig(getAgentLevel(index)).textColor
|
||||
}"
|
||||
>
|
||||
{{ getAgentLevel(index) }}
|
||||
</div>
|
||||
|
||||
<!-- 智能体名称 -->
|
||||
<div class="agent-name">{{ agent.Name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="agents.length === 0" class="empty-state">
|
||||
<el-empty description="暂无智能体数据" />
|
||||
<div class="empty-tip">请先在智能体库中上传智能体信息</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.agent-allocation {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.allocation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: var(--color-text-title);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
padding: 4px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
margin: 0;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.allocation-content {
|
||||
flex: 1;
|
||||
padding: 30px 20px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
// 椭圆框容器
|
||||
.ellipse-box-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
|
||||
// 椭圆框样式
|
||||
.ellipse-box {
|
||||
position: absolute;
|
||||
border: 2px solid #409eff;
|
||||
border-radius: 50px; // 椭圆形状
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(64, 158, 255, 0.1) 0%,
|
||||
rgba(64, 158, 255, 0.05) 100%
|
||||
);
|
||||
pointer-events: none; // 允许点击穿透
|
||||
z-index: 1;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
opacity: 0;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 椭圆框动画
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid rgba(64, 158, 255, 0.3);
|
||||
border-radius: 50px;
|
||||
animation: pulse 2s infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agents-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
z-index: 2; // 确保头像在椭圆框上方
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 80px;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
cursor: pointer;
|
||||
|
||||
// 选中状态的动画效果
|
||||
&.selected {
|
||||
transform: scale(1.05);
|
||||
z-index: 10;
|
||||
|
||||
// 让选中头像移动到前面
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.agent-avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid var(--color-border);
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
// 选中指示器
|
||||
.selected-indicator {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
width: 100%;
|
||||
|
||||
.empty-tip {
|
||||
margin-top: 16px;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 椭圆框脉冲动画
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
padding: 20px 16px;
|
||||
gap: 16px;
|
||||
|
||||
.agents-container {
|
||||
gap: 12px;
|
||||
|
||||
.agent-item {
|
||||
width: 70px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.agent-allocation {
|
||||
.allocation-content {
|
||||
.agents-container {
|
||||
gap: 8px;
|
||||
|
||||
.agent-item {
|
||||
width: 65px;
|
||||
|
||||
.agent-avatar {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.color-level {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,533 @@
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import { getAgentMapIcon } from '@/layout/components/config.ts'
|
||||
import { type ConnectArg, Jsplumb } from '@/layout/components/Main/TaskTemplate/utils.ts'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import { computed, ref } from 'vue'
|
||||
import { AnchorLocations } from '@jsplumb/browser-ui'
|
||||
import MultiLineTooltip from '@/components/MultiLineTooltip/index.vue'
|
||||
import Bg from './Bg.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(el: 'resetAgentRepoLine'): void
|
||||
(el: 'setCurrentTask', task: IRawStepTask): void
|
||||
}>()
|
||||
|
||||
const jsplumb = new Jsplumb('task-syllabus')
|
||||
|
||||
const handleScroll = () => {
|
||||
emit('resetAgentRepoLine')
|
||||
}
|
||||
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
const collaborationProcess = computed(() => {
|
||||
return agentsStore.agentRawPlan.data?.['Collaboration Process'] ?? []
|
||||
})
|
||||
|
||||
// 编辑状态管理
|
||||
const editingTaskId = ref<string | null>(null)
|
||||
const editingContent = ref('')
|
||||
|
||||
// 添加新产物状态管理
|
||||
const showAddOutputForm = ref(false)
|
||||
const newOutputName = ref('')
|
||||
|
||||
// 开始编辑
|
||||
const startEditing = (task: IRawStepTask) => {
|
||||
if (!task.Id) {
|
||||
console.warn('Task ID is missing, cannot start editing')
|
||||
return
|
||||
}
|
||||
editingTaskId.value = task.Id
|
||||
editingContent.value = task.TaskContent || ''
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
const saveEditing = () => {
|
||||
if (editingTaskId.value && editingContent.value.trim()) {
|
||||
const taskToUpdate = collaborationProcess.value.find(item => item.Id === editingTaskId.value)
|
||||
if (taskToUpdate) {
|
||||
taskToUpdate.TaskContent = editingContent.value.trim()
|
||||
}
|
||||
}
|
||||
editingTaskId.value = null
|
||||
editingContent.value = ''
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const cancelEditing = () => {
|
||||
editingTaskId.value = null
|
||||
editingContent.value = ''
|
||||
}
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
saveEditing()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelEditing()
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加产物表单
|
||||
const showAddOutputDialog = () => {
|
||||
showAddOutputForm.value = true
|
||||
newOutputName.value = ''
|
||||
}
|
||||
|
||||
// 添加新产物
|
||||
const addNewOutput = () => {
|
||||
if (newOutputName.value.trim()) {
|
||||
const success = agentsStore.addNewOutput(newOutputName.value.trim())
|
||||
if (success) {
|
||||
showAddOutputForm.value = false
|
||||
newOutputName.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消添加产物
|
||||
const cancelAddOutput = () => {
|
||||
showAddOutputForm.value = false
|
||||
newOutputName.value = ''
|
||||
}
|
||||
|
||||
// 处理添加产物的键盘事件
|
||||
const handleAddOutputKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
addNewOutput()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelAddOutput()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除额外产物
|
||||
const removeAdditionalOutput = (output: string) => {
|
||||
agentsStore.removeAdditionalOutput(output)
|
||||
}
|
||||
|
||||
function handleCurrentTask(task: IRawStepTask, transparent: boolean): ConnectArg[] {
|
||||
// 创建当前流程与产出的连线
|
||||
const arr: ConnectArg[] = [
|
||||
{
|
||||
sourceId: `task-syllabus-flow-${task.Id}`,
|
||||
targetId: `task-syllabus-output-object-${task.Id}`,
|
||||
anchor: [AnchorLocations.Right, AnchorLocations.Left],
|
||||
config: {
|
||||
transparent
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 创建当前产出与流程的连线
|
||||
task.InputObject_List?.forEach(item => {
|
||||
const id = collaborationProcess.value.find(i => i.OutputObject === item)?.Id
|
||||
if (id) {
|
||||
arr.push({
|
||||
sourceId: `task-syllabus-output-object-${id}`,
|
||||
targetId: `task-syllabus-flow-${task.Id}`,
|
||||
anchor: [AnchorLocations.Left, AnchorLocations.Right],
|
||||
config: {
|
||||
type: 'output',
|
||||
transparent
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return arr
|
||||
}
|
||||
|
||||
function changeTask(task?: IRawStepTask, isEmit?: boolean) {
|
||||
jsplumb.reset()
|
||||
const arr: ConnectArg[] = []
|
||||
agentsStore.agentRawPlan.data?.['Collaboration Process']?.forEach(item => {
|
||||
arr.push(...handleCurrentTask(item, item.Id !== task?.Id))
|
||||
})
|
||||
jsplumb.connects(arr)
|
||||
if (isEmit && task) {
|
||||
emit('setCurrentTask', task)
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
jsplumb.reset()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
changeTask,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="text-[18px] font-bold mb-[18px] text-[var(--color-text-title-header)]">
|
||||
任务大纲
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-loading="agentsStore.agentRawPlan.loading"
|
||||
class="flex-1 w-full overflow-y-auto relative"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
v-show="collaborationProcess.length > 0"
|
||||
class="w-full relative min-h-full"
|
||||
id="task-syllabus"
|
||||
>
|
||||
<Bg />
|
||||
|
||||
<div class="w-full flex items-center gap-[14%] mb-[35px]">
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div
|
||||
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
|
||||
>
|
||||
流程
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex justify-center">
|
||||
<div
|
||||
class="card-item w-[168px] h-[41px] flex justify-center relative z-99 items-center rounded-[20px] bg-[var(--color-bg-flow)]"
|
||||
>
|
||||
产物
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="item in collaborationProcess"
|
||||
:key="item.Id"
|
||||
class="card-it w-full flex items-center gap-[14%] bg-[var(--color-card-bg)]"
|
||||
>
|
||||
<!-- 流程卡片 -->
|
||||
<el-card
|
||||
class="w-[43%] overflow-y-auto relative z-99 task-syllabus-flow-card"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:shadow="true"
|
||||
:id="`task-syllabus-flow-${item.Id}`"
|
||||
@click="changeTask(item, true)"
|
||||
>
|
||||
<MultiLineTooltip placement="right" :text="item.StepName" :lines="2">
|
||||
<div class="text-[18px] font-bold text-center">{{ item.StepName }}</div>
|
||||
</MultiLineTooltip>
|
||||
<div class="h-[1px] w-full bg-[#d6d6d6] my-[8px]"></div>
|
||||
|
||||
<!-- 任务内容区域 - 支持双击编辑 -->
|
||||
<div v-if="editingTaskId === item.Id" class="w-full">
|
||||
<div class="flex flex-col gap-3">
|
||||
<el-input
|
||||
v-model="editingContent"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="请输入任务内容"
|
||||
@keydown="handleKeydown"
|
||||
class="task-content-editor"
|
||||
size="small"
|
||||
/>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="saveEditing" type="primary" size="small" class="px-3">
|
||||
√
|
||||
</el-button>
|
||||
<el-button @click="cancelEditing" size="small" class="px-3"> × </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else @dblclick="startEditing(item)" class="w-full cursor-pointer">
|
||||
<MultiLineTooltip placement="right" :text="item.TaskContent" :lines="3">
|
||||
<div class="text-[14px] text-[var(--color-text-secondary)] task-content-display">
|
||||
{{ item.TaskContent }}
|
||||
</div>
|
||||
</MultiLineTooltip>
|
||||
</div>
|
||||
|
||||
<div class="h-[1px] w-full bg-[#d6d6d6] my-[8px]"></div>
|
||||
<div
|
||||
class="flex items-center gap-2 overflow-y-auto flex-wrap relative w-full max-h-[72px]"
|
||||
>
|
||||
<!-- 连接到智能体库的连接点 -->
|
||||
<div
|
||||
class="absolute left-[-10px] top-1/2 transform -translate-y-1/2"
|
||||
:id="`task-syllabus-flow-agents-${item.Id}`"
|
||||
></div>
|
||||
<el-tooltip
|
||||
v-for="agentSelection in item.AgentSelection"
|
||||
:key="agentSelection"
|
||||
effect="light"
|
||||
placement="right"
|
||||
>
|
||||
<template #content>
|
||||
<div class="w-[150px]">
|
||||
<div class="text-[18px] font-bold">{{ agentSelection }}</div>
|
||||
<div class="h-[1px] w-full bg-[#494B51] my-[8px]"></div>
|
||||
<div>
|
||||
{{ item.TaskProcess.find(i => i.AgentName === agentSelection)?.Description }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="w-[31px] h-[31px] rounded-full flex items-center justify-center"
|
||||
:style="{ background: getAgentMapIcon(agentSelection).color }"
|
||||
>
|
||||
<svg-icon
|
||||
:icon-class="getAgentMapIcon(agentSelection).icon"
|
||||
color="#fff"
|
||||
size="24px"
|
||||
/>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 产物卡片 -->
|
||||
<el-card
|
||||
class="w-[43%] relative task-syllabus-output-object-card"
|
||||
:shadow="true"
|
||||
:class="agentsStore.currentTask?.StepName === item.StepName ? 'active-card' : ''"
|
||||
:id="`task-syllabus-output-object-${item.Id}`"
|
||||
>
|
||||
<div class="text-[18px] font-bold text-center">{{ item.OutputObject }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 额外的产物列表 -->
|
||||
<!-- <div
|
||||
v-for="(output, index) in agentsStore.additionalOutputs"
|
||||
:key="`additional-${index}`"
|
||||
class="card-item w-full flex items-center gap-[14%]"
|
||||
> -->
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<!-- <div class="w-[43%]"></div> -->
|
||||
<!-- 额外产物卡片 -->
|
||||
<!-- <el-card class="w-[43%] relative additional-output-card group" :shadow="false">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-[18px] font-bold text-center flex-1">{{ output }}</div>
|
||||
<button
|
||||
@click="removeAdditionalOutput(output)"
|
||||
class="opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-700 transition-all duration-200 ml-2 text-lg font-bold w-5 h-5 flex items-center justify-center rounded hover:bg-red-50"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div> -->
|
||||
<!-- 添加新产物按钮 -->
|
||||
<!-- <div class="card-item w-full flex items-center gap-[14%] mt-[20px]"> -->
|
||||
<!-- 空的流程卡片位置 -->
|
||||
<!-- <div class="w-[43%]"></div> -->
|
||||
<!-- 添加新产物按钮 -->
|
||||
<!-- <div class="w-[43%] relative"> -->
|
||||
<!-- <div v-if="!showAddOutputForm" class="add-output-btn">
|
||||
<button
|
||||
@click="showAddOutputDialog"
|
||||
class="w-full h-[50px] border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center text-gray-500 hover:border-blue-500 hover:text-blue-500 transition-all duration-200 group"
|
||||
>
|
||||
<svg-icon icon-class="plus" size="20px" class="mr-2" />
|
||||
<span class="text-sm font-medium"></span>
|
||||
</button>
|
||||
</div> -->
|
||||
|
||||
<!-- 添加产物表单 -->
|
||||
<!-- <div v-else class="add-output-form">
|
||||
<el-card class="w-full" shadow="hover">
|
||||
<div class="p-3">
|
||||
<el-input
|
||||
v-model="newOutputName"
|
||||
placeholder="输入产物名称"
|
||||
size="small"
|
||||
@keydown="handleAddOutputKeydown"
|
||||
@blur="addNewOutput"
|
||||
class="w-full mb-3"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<el-button @click="addNewOutput" type="primary" size="small" class="flex-1">
|
||||
确定
|
||||
</el-button>
|
||||
<el-button @click="cancelAddOutput" size="small" class="flex-1">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div>
|
||||
<el-button type="info" size="medium" class="flex-1"> branch </el-button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.task-syllabus-flow-card {
|
||||
background-color: var(--color-card-bg-task);
|
||||
border: 1px solid var(--color-card-border-task);
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease;
|
||||
// box-shadow: var(--color-card-shadow);
|
||||
margin-bottom: 100px;
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-task-hover);
|
||||
border-color: var(--color-card-border-hover);
|
||||
box-shadow: var(--color-card-shadow-hover);
|
||||
}
|
||||
:deep(.el-card__body) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.task-syllabus-output-object-card {
|
||||
background-color: var(--color-card-bg-task);
|
||||
border: 1px solid var(--color-card-border-task);
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.2s ease;
|
||||
// box-shadow: var(--color-card-shadow-hover);
|
||||
&:hover {
|
||||
background-color: var(--color-card-bg-task-hover);
|
||||
border-color: var(--color-card-border-hover);
|
||||
box-shadow: var(--color-card-shadow-hover);
|
||||
}
|
||||
:deep(.el-card__body) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.task-content-editor {
|
||||
:deep(.el-textarea__inner) {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-content-display {
|
||||
min-height: 40px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.add-output-btn {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-output-form {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加产物表单样式
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
:deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #c0c4cc;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
// 任务内容编辑按钮样式 - 匹配执行结果编辑按钮样式
|
||||
.task-content-editor {
|
||||
.el-button {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 在深色模式下,按钮背景会自动适配为深色
|
||||
html.dark & {
|
||||
.el-button {
|
||||
&.el-button--primary {
|
||||
background-color: var(--color-bg-detail);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
border-color: var(--color-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.el-button--primary) {
|
||||
background-color: var(--color-bg-detail);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-bg-hover);
|
||||
border-color: var(--color-text-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
frontend/src/layout/components/Main/TaskTemplate/index.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import AgentRepo from './AgentRepo/index.vue'
|
||||
import TaskSyllabus from './TaskSyllabus/index.vue'
|
||||
import TaskResult from './TaskResult/index.vue'
|
||||
import { Jsplumb } from './utils.ts'
|
||||
import { type IRawStepTask, useAgentsStore } from '@/stores'
|
||||
import { BezierConnector } from '@jsplumb/browser-ui'
|
||||
import { ref } from 'vue'
|
||||
const agentsStore = useAgentsStore()
|
||||
|
||||
// 智能体库
|
||||
const agentRepoJsplumb = new Jsplumb('task-template', {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: {
|
||||
curviness: 30, // 曲线弯曲程度
|
||||
stub: 20, // 添加连接点与端点的距离
|
||||
alwaysRespectStubs: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 任务流程
|
||||
const taskSyllabusRef = ref<{
|
||||
changeTask: (task?: IRawStepTask, isEmit?: boolean) => void
|
||||
clear: () => void
|
||||
}>()
|
||||
// 执行结果
|
||||
const taskResultRef = ref<{
|
||||
createInternalLine: () => void
|
||||
clear: () => void
|
||||
}>()
|
||||
const taskResultJsplumb = new Jsplumb('task-template')
|
||||
|
||||
function scrollToElementTop(elementId: string) {
|
||||
const element = document.getElementById(elementId)
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleTaskSyllabusCurrentTask(task: IRawStepTask) {
|
||||
scrollToElementTop(`task-results-${task.Id}-0`)
|
||||
agentsStore.setCurrentTask(task)
|
||||
}
|
||||
|
||||
function handleTaskResultCurrentTask(task: IRawStepTask) {
|
||||
scrollToElementTop(`task-syllabus-flow-${task.Id}`)
|
||||
agentsStore.setCurrentTask(task)
|
||||
// 更新任务大纲内部的线
|
||||
taskSyllabusRef.value?.changeTask(task, false)
|
||||
}
|
||||
|
||||
function changeTask() {
|
||||
taskResultRef.value?.createInternalLine()
|
||||
taskSyllabusRef.value?.changeTask()
|
||||
}
|
||||
|
||||
function resetAgentRepoLine() {
|
||||
agentRepoJsplumb.repaintEverything()
|
||||
taskResultJsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
taskSyllabusRef.value?.clear()
|
||||
taskResultRef.value?.clear()
|
||||
agentRepoJsplumb.repaintEverything()
|
||||
taskResultJsplumb.repaintEverything()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
changeTask,
|
||||
resetAgentRepoLine,
|
||||
clear
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 删除overflow-hidden -->
|
||||
<div
|
||||
class="task-template flex gap-6 items-center h-[calc(100%-84px)] relative"
|
||||
id="task-template"
|
||||
>
|
||||
<!-- 智能体库 -->
|
||||
<div class="w-[9.5%] min-w-[179px] h-full relative flex-shrink-0">
|
||||
<AgentRepo @resetAgentRepoLine="agentRepoJsplumb.repaintEverything" />
|
||||
</div>
|
||||
<!-- 任务大纲 -->
|
||||
<div class="w-[35.5%] min-w-[600px] h-full px-[20px] flex-shrink-0">
|
||||
<TaskSyllabus
|
||||
ref="taskSyllabusRef"
|
||||
@resetAgentRepoLine="resetAgentRepoLine"
|
||||
@set-current-task="handleTaskSyllabusCurrentTask"
|
||||
/>
|
||||
</div>
|
||||
<!-- 执行结果 -->
|
||||
<div class="flex-1 h-full">
|
||||
<TaskResult
|
||||
ref="taskResultRef"
|
||||
@refresh-line="taskResultJsplumb.repaintEverything"
|
||||
@set-current-task="handleTaskResultCurrentTask"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.task-template {
|
||||
& > div {
|
||||
box-shadow: var(--color-card-shadow-three);
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--color-card-border-three);
|
||||
background: var(--color-bg-three);
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
169
frontend/src/layout/components/Main/TaskTemplate/utils.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type {
|
||||
AnchorSpec,
|
||||
ConnectorSpec,
|
||||
ConnectParams,
|
||||
EndpointOptions,
|
||||
JsPlumbInstance,
|
||||
} from '@jsplumb/browser-ui'
|
||||
import { BezierConnector, DotEndpoint, newInstance } from '@jsplumb/browser-ui'
|
||||
|
||||
export interface JsplumbConfig {
|
||||
connector?: ConnectorSpec
|
||||
type?: 'input' | 'output'
|
||||
stops?: [[number, string], [number, string]]
|
||||
// 连接线条是否变透明一些
|
||||
transparent?: boolean
|
||||
}
|
||||
|
||||
export interface ConnectArg {
|
||||
sourceId: string
|
||||
targetId: string
|
||||
anchor: AnchorSpec
|
||||
config?: JsplumbConfig
|
||||
}
|
||||
|
||||
const defaultConfig: JsplumbConfig = {
|
||||
connector: {
|
||||
type: BezierConnector.type,
|
||||
options: {
|
||||
curviness: 70,
|
||||
stub: 10,
|
||||
},
|
||||
},
|
||||
type: 'input',
|
||||
}
|
||||
|
||||
export class Jsplumb {
|
||||
instance!: JsPlumbInstance
|
||||
containerId: string
|
||||
config: JsplumbConfig
|
||||
|
||||
constructor(eleId: string, config = {} as JsplumbConfig) {
|
||||
this.containerId = eleId
|
||||
this.config = { ...defaultConfig, ...config }
|
||||
onMounted(() => {
|
||||
this.init()
|
||||
})
|
||||
}
|
||||
|
||||
init = () => {
|
||||
if (this.instance) {
|
||||
return
|
||||
}
|
||||
this.instance = newInstance({
|
||||
container: document.querySelector(`#${this.containerId}`)!, // 或指定共同的父容器
|
||||
})
|
||||
}
|
||||
|
||||
getStops = (type?: 'input' | 'output'): [[number, string], [number, string]] => {
|
||||
if (type === 'input') {
|
||||
return [
|
||||
[0, '#FF6161'],
|
||||
[1, '#D76976'],
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
[0, '#0093EB'],
|
||||
[1, '#00D2D1'],
|
||||
]
|
||||
}
|
||||
|
||||
_connect = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
anchor: AnchorSpec,
|
||||
_config = {} as JsplumbConfig,
|
||||
) => {
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
...this.config,
|
||||
..._config,
|
||||
}
|
||||
this.init()
|
||||
// 连接两个元素
|
||||
const sourceElement = document.querySelector(`#${sourceId}`)
|
||||
const targetElement = document.querySelector(`#${targetId}`)
|
||||
const stops = _config.stops ?? this.getStops(config.type)
|
||||
// 如果config.transparent为true,则将stops都加一些透明度
|
||||
if (config.transparent) {
|
||||
stops[0][1] = stops[0][1] + '80'
|
||||
stops[1][1] = stops[1][1] + '80'
|
||||
}
|
||||
if (targetElement && sourceElement) {
|
||||
this.instance.connect({
|
||||
source: sourceElement,
|
||||
target: targetElement,
|
||||
connector: config.connector,
|
||||
anchor: anchor,
|
||||
paintStyle: {
|
||||
stroke: stops[0][1],
|
||||
strokeWidth: 2.5,
|
||||
dashstyle: '0',
|
||||
zIndex: 100,
|
||||
opacity: 0.9,
|
||||
gradient: {
|
||||
stops: stops,
|
||||
type: 'linear',
|
||||
},
|
||||
},
|
||||
sourceEndpointStyle: { fill: stops[0][1] },
|
||||
|
||||
endpoint: {
|
||||
type: DotEndpoint.type,
|
||||
options: { radius: 5 },
|
||||
},
|
||||
cssClass: `jtk-connector-${config.type}`
|
||||
} as unknown as ConnectParams<unknown>)
|
||||
|
||||
// 为源元素添加端点
|
||||
this.instance.addEndpoint(sourceElement, {
|
||||
anchor: (anchor as [AnchorSpec, AnchorSpec])[0],
|
||||
paintStyle: { fill: stops[0][1], zIndex: 100 }, // source端点颜色
|
||||
} as unknown as EndpointOptions)
|
||||
|
||||
// 为目标元素添加端点
|
||||
this.instance.addEndpoint(targetElement, {
|
||||
anchor: (anchor as [AnchorSpec, AnchorSpec])[1],
|
||||
paintStyle: { fill: stops[1][1], zIndex: 100 }, // target端点颜色
|
||||
} as unknown as EndpointOptions)
|
||||
}
|
||||
}
|
||||
|
||||
connect = (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
anchor: AnchorSpec,
|
||||
config = {} as JsplumbConfig,
|
||||
) => {
|
||||
this._connect(sourceId, targetId, anchor, config)
|
||||
}
|
||||
|
||||
connects = (args: ConnectArg[]) => {
|
||||
this.instance.batch(() => {
|
||||
args.forEach(({ sourceId, targetId, anchor, config }) => {
|
||||
this._connect(sourceId, targetId, anchor, config)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
repaintEverything = () => {
|
||||
// 重新验证元素位置
|
||||
const container = document.querySelector(`#${this.containerId}`)
|
||||
if (container) {
|
||||
const elements = container.querySelectorAll('[id^="task-results-"]')
|
||||
elements.forEach((element) => {
|
||||
this.instance.revalidate(element)
|
||||
})
|
||||
}
|
||||
this.instance.repaintEverything()
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
this.instance.deleteEveryConnection()
|
||||
const allEndpoints = this.instance.selectEndpoints()
|
||||
allEndpoints.each((endpoint) => {
|
||||
this.instance.deleteEndpoint(endpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
22
frontend/src/layout/components/Main/index.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import Task from './Task.vue'
|
||||
import TaskTemplate from './TaskTemplate/index.vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
|
||||
const taskTemplateRef = ref<{ changeTask: () => void; clear: () => void }>()
|
||||
|
||||
function handleSearch() {
|
||||
nextTick(() => {
|
||||
taskTemplateRef.value?.changeTask()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-[24px] h-[calc(100%-60px)]">
|
||||
<Task @search="handleSearch" @search-start="taskTemplateRef?.clear" />
|
||||
<TaskTemplate ref="taskTemplateRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
150
frontend/src/layout/components/config.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
export interface AgentMapIcon {
|
||||
name: string
|
||||
icon: string
|
||||
color: string
|
||||
}
|
||||
// "肾脏病学家": {
|
||||
// name: "肾脏病学家",
|
||||
// icon: "doctor",
|
||||
// color: "#00A2D2",
|
||||
// },
|
||||
export const agentMapIcon = new Map<string, AgentMapIcon>()
|
||||
agentMapIcon.set("船舶设计师", {
|
||||
name: '船舶设计师',
|
||||
icon: 'shejishi',
|
||||
color: '#65AE00',
|
||||
})
|
||||
agentMapIcon.set("防护工程专家", {
|
||||
name: '防护工程专家',
|
||||
icon: 'engineer',
|
||||
color: '#B06CFE',
|
||||
})
|
||||
agentMapIcon.set("病理生理学家", {
|
||||
name: '病理生理学家',
|
||||
icon: 'doctor',
|
||||
color: '#00A2D2',
|
||||
})
|
||||
agentMapIcon.set("药物化学家", {
|
||||
name: '药物化学家',
|
||||
icon: 'specialist',
|
||||
color: '#FF7914',
|
||||
})
|
||||
agentMapIcon.set("制剂工程师", {
|
||||
name: '制剂工程师',
|
||||
icon: 'medical',
|
||||
color: '#65AE00',
|
||||
})
|
||||
agentMapIcon.set("监管事务专家", {
|
||||
name: '监管事务专家',
|
||||
icon: 'researcher',
|
||||
color: '#0064C4',
|
||||
})
|
||||
agentMapIcon.set("物理学家", {
|
||||
name: '物理学家',
|
||||
icon: 'specialist',
|
||||
color: '#B06CFE',
|
||||
})
|
||||
agentMapIcon.set("实验材料学家", {
|
||||
name: '实验材料学家',
|
||||
icon: 'researcher',
|
||||
color: '#C01E6A',
|
||||
})
|
||||
agentMapIcon.set("计算模拟专家", {
|
||||
name: '计算模拟专家',
|
||||
icon: 'researcher',
|
||||
color: '#FF7914',
|
||||
})
|
||||
agentMapIcon.set("腐蚀机理研究员", {
|
||||
name: '腐蚀机理研究员',
|
||||
icon: 'specialist',
|
||||
color: '#00C8D2',
|
||||
})
|
||||
agentMapIcon.set("先进材料研发员", {
|
||||
name: '先进材料研发员',
|
||||
icon: 'engineer',
|
||||
color: '#00C8D2',
|
||||
})
|
||||
agentMapIcon.set("肾脏病学家", {
|
||||
name: '肾脏病学家',
|
||||
icon: 'doctor',
|
||||
color: '#00A2D2',
|
||||
})
|
||||
agentMapIcon.set("临床研究协调员", {
|
||||
name: '临床研究协调员',
|
||||
icon: 'renyuan',
|
||||
color: '#FF7914',
|
||||
})
|
||||
agentMapIcon.set("中医药专家", {
|
||||
name: '中医药专家',
|
||||
icon: 'medical',
|
||||
color: '#00C8D2',
|
||||
})
|
||||
agentMapIcon.set("药物安全专家", {
|
||||
name: '药物安全专家',
|
||||
icon: 'medical',
|
||||
color: '#65AE00',
|
||||
})
|
||||
agentMapIcon.set("二维材料科学家", {
|
||||
name: '二维材料科学家',
|
||||
icon: 'shejishi',
|
||||
color: '#EB6363',
|
||||
})
|
||||
agentMapIcon.set("光电物理学家", {
|
||||
name: '光电物理学家',
|
||||
icon: 'specialist',
|
||||
color: '#079EFF',
|
||||
})
|
||||
agentMapIcon.set("机器学习专家", {
|
||||
name: '机器学习专家',
|
||||
icon: 'researcher',
|
||||
color: '#8700AE',
|
||||
})
|
||||
agentMapIcon.set("流体动力学专家", {
|
||||
name: '流体动力学专家',
|
||||
icon: 'specialist',
|
||||
color: '#EB6363',
|
||||
})
|
||||
|
||||
export function getAgentMapIcon(agentName: string):AgentMapIcon {
|
||||
return agentMapIcon.get(agentName) ?? agentMapIcon.get('监管事务专家')!
|
||||
}
|
||||
|
||||
export interface AgentMapDuty {
|
||||
name: string
|
||||
key: string
|
||||
color: string
|
||||
border: string
|
||||
}
|
||||
|
||||
// 职责映射
|
||||
// 提议 评审 改进 总结
|
||||
export const agentMapDuty: Record<string, AgentMapDuty> = {
|
||||
Propose: {
|
||||
name: '提议',
|
||||
key: 'propose',
|
||||
color: '#06A3FF',
|
||||
border:'#0f95e4'
|
||||
},
|
||||
Critique: {
|
||||
name: '评审',
|
||||
key: 'review',
|
||||
color: '#2cd235',
|
||||
border:'#19ab21'
|
||||
},
|
||||
Improve: {
|
||||
name: '改进',
|
||||
key: 'improve',
|
||||
color: '#ff8a01',
|
||||
border:'#dc7700'
|
||||
},
|
||||
Finalize: {
|
||||
name: '总结',
|
||||
key: 'summary',
|
||||
color: '#bf65ff',
|
||||
border:'#a13de8'
|
||||
},
|
||||
}
|
||||
|
||||
export function getActionTypeDisplay(type: string) {
|
||||
return agentMapDuty[type]
|
||||
}
|
||||
84
frontend/src/layout/index.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import Header from './components/Header.vue'
|
||||
import Main from './components/Main/index.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const isDarkMode = ref(false)
|
||||
|
||||
// 初始化主题
|
||||
const initTheme = () => {
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
enableDarkMode()
|
||||
} else {
|
||||
enableLightMode()
|
||||
}
|
||||
}
|
||||
|
||||
// 启用深色模式
|
||||
const enableDarkMode = () => {
|
||||
document.documentElement.classList.add('dark')
|
||||
isDarkMode.value = true
|
||||
localStorage.setItem('theme', 'dark')
|
||||
}
|
||||
|
||||
// 启用浅色模式
|
||||
const enableLightMode = () => {
|
||||
document.documentElement.classList.remove('dark')
|
||||
isDarkMode.value = false
|
||||
localStorage.setItem('theme', 'light')
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
if (isDarkMode.value) {
|
||||
enableLightMode()
|
||||
} else {
|
||||
enableDarkMode()
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化主题
|
||||
onMounted(() => {
|
||||
initTheme()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full relative">
|
||||
<!-- 主题切换按钮 - 放置在页面右上角 -->
|
||||
<div class="absolute top-4 right-4 z-50">
|
||||
<button
|
||||
@click="toggleTheme"
|
||||
class="w-10 h-10 rounded-full transition-all duration-300 flex items-center justify-center"
|
||||
:class="{
|
||||
'bg-[#ebebeb] hover:bg-[var(--color-bg-hover)]': !isDarkMode,
|
||||
'bg-[#0e131c] hover:bg-[var(--color-bg-hover)]': isDarkMode
|
||||
}"
|
||||
:title="isDarkMode ? '切换到浅色模式' : '切换到深色模式'"
|
||||
>
|
||||
<!-- 浅色模式图标 -->
|
||||
<svg-icon
|
||||
v-if="!isDarkMode"
|
||||
icon-class="sunny"
|
||||
size="20px"
|
||||
:color="isDarkMode ? 'var(--color-text-title)' : 'var(--color-text-title)'"
|
||||
class="transition-opacity duration-300"
|
||||
/>
|
||||
<!-- 深色模式图标 -->
|
||||
<svg-icon
|
||||
v-else
|
||||
icon-class="moon"
|
||||
size="20px"
|
||||
:color="isDarkMode ? 'var(--color-text-title)' : 'var(--color-text-title)'"
|
||||
class="transition-opacity duration-300"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Header />
|
||||
<Main />
|
||||
</div>
|
||||
</template>
|
||||
23
frontend/src/main.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/index.scss'
|
||||
import './styles/tailwindcss.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import 'virtual:svg-icons-register'
|
||||
import { initService } from '@/utils/request.ts'
|
||||
import { setupStore, useConfigStore } from '@/stores'
|
||||
|
||||
async function init() {
|
||||
const app = createApp(App)
|
||||
setupStore(app)
|
||||
initService()
|
||||
const configStore = useConfigStore()
|
||||
await configStore.initConfig()
|
||||
document.title = configStore.config.centerTitle
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
void init()
|
||||
8
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [],
|
||||
})
|
||||
|
||||
export default router
|
||||
12
frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import type { App } from 'vue'
|
||||
|
||||
export const store = createPinia()
|
||||
|
||||
// 全局注册 store
|
||||
export function setupStore(app: App<Element>) {
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
export * from './modules/agents.ts'
|
||||
export * from './modules/config.ts'
|
||||
185
frontend/src/stores/modules/agents.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { store } from '../index'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { IExecuteRawResponse } from '@/api'
|
||||
import { useConfigStore } from '@/stores/modules/config.ts'
|
||||
|
||||
export interface Agent {
|
||||
Name: string
|
||||
Profile: string
|
||||
Icon: string
|
||||
Classification: string
|
||||
}
|
||||
|
||||
type HslColorVector = [number, number, number]
|
||||
|
||||
export interface IRichText {
|
||||
template: string
|
||||
data: Record<
|
||||
string,
|
||||
{
|
||||
text: string
|
||||
style?: Record<string, string>
|
||||
color?: HslColorVector
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export interface TaskProcess {
|
||||
ActionType: string
|
||||
AgentName: string
|
||||
Description: string
|
||||
ID: string
|
||||
ImportantInput: string[]
|
||||
}
|
||||
|
||||
export interface IRawStepTask {
|
||||
Id?: string
|
||||
StepName?: string
|
||||
TaskContent?: string
|
||||
InputObject_List?: string[]
|
||||
OutputObject?: string
|
||||
AgentSelection?: string[]
|
||||
Collaboration_Brief_FrontEnd: IRichText
|
||||
TaskProcess: TaskProcess[]
|
||||
}
|
||||
|
||||
export interface IRawPlanResponse {
|
||||
'Initial Input Object'?: string[] | string
|
||||
'General Goal'?: string
|
||||
'Collaboration Process'?: IRawStepTask[]
|
||||
}
|
||||
|
||||
const storageKey = '$agents' as const
|
||||
|
||||
// 清除所有以 storageKey 开头的 localStorage
|
||||
function clearStorageByVersion() {
|
||||
Object.keys(localStorage)
|
||||
.filter((key) => key.startsWith(storageKey))
|
||||
.forEach((key) => localStorage.removeItem(key))
|
||||
}
|
||||
|
||||
export const useAgentsStore = defineStore('agents', () => {
|
||||
const configStore = useConfigStore()
|
||||
const agents = useStorage<Agent[]>(`${storageKey}-repository`, [])
|
||||
function setAgents(agent: Agent[]) {
|
||||
agents.value = agent
|
||||
};
|
||||
|
||||
// 任务搜索的内容
|
||||
const searchValue = useStorage<string>(`${storageKey}-search-value`, '')
|
||||
function setSearchValue(value: string) {
|
||||
searchValue.value = value
|
||||
}
|
||||
|
||||
const storageVersionIdentifier = useStorage<string>(`${storageKey}-storage-version-identifier`, '')
|
||||
// 监听 configStore.config.agentRepository.storageVersionIdentifier 改变
|
||||
watch(
|
||||
() => configStore.config.agentRepository.storageVersionIdentifier,
|
||||
(value) => {
|
||||
// value与storageVersionIdentifier不一致清除所有storageKey开头的localStorage
|
||||
if (value !== storageVersionIdentifier.value) {
|
||||
clearStorageByVersion()
|
||||
storageVersionIdentifier.value = value
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 当前的展示的任务流程
|
||||
const currentTask = ref<IRawStepTask>()
|
||||
function setCurrentTask(task: IRawStepTask) {
|
||||
currentTask.value = task
|
||||
}
|
||||
|
||||
const agentRawPlan = ref<{ data?: IRawPlanResponse; loading?: boolean }>({ loading: false })
|
||||
|
||||
function setAgentRawPlan(plan: { data?: IRawPlanResponse; loading?: boolean }) {
|
||||
if (plan.data) {
|
||||
plan.data['Collaboration Process'] = plan.data['Collaboration Process']?.map((item) => ({
|
||||
...item,
|
||||
Id: uuidv4(),
|
||||
}))
|
||||
}
|
||||
agentRawPlan.value = {
|
||||
...agentRawPlan.value,
|
||||
...plan,
|
||||
}
|
||||
}
|
||||
|
||||
// 执行完任务的结果
|
||||
const executePlan = ref<IExecuteRawResponse[]>([])
|
||||
function setExecutePlan(plan: IExecuteRawResponse[]) {
|
||||
executePlan.value = plan
|
||||
}
|
||||
|
||||
function resetAgent() {
|
||||
agentRawPlan.value = {
|
||||
loading: false,
|
||||
}
|
||||
currentTask.value = undefined
|
||||
executePlan.value = []
|
||||
}
|
||||
|
||||
// 额外的产物列表
|
||||
const additionalOutputs = ref<string[]>([])
|
||||
|
||||
// 添加新产物
|
||||
function addNewOutput(outputObject: string) {
|
||||
if (!outputObject.trim()) return false
|
||||
|
||||
const trimmedOutput = outputObject.trim()
|
||||
if (!additionalOutputs.value.includes(trimmedOutput)) {
|
||||
additionalOutputs.value.push(trimmedOutput)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 删除额外产物
|
||||
function removeAdditionalOutput(outputObject: string) {
|
||||
const index = additionalOutputs.value.indexOf(outputObject)
|
||||
if (index > -1) {
|
||||
additionalOutputs.value.splice(index, 1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 清除额外产物
|
||||
function clearAdditionalOutputs() {
|
||||
additionalOutputs.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
agents,
|
||||
setAgents,
|
||||
searchValue,
|
||||
setSearchValue,
|
||||
currentTask,
|
||||
setCurrentTask,
|
||||
agentRawPlan,
|
||||
setAgentRawPlan,
|
||||
executePlan,
|
||||
setExecutePlan,
|
||||
resetAgent,
|
||||
additionalOutputs,
|
||||
addNewOutput,
|
||||
removeAdditionalOutput,
|
||||
clearAdditionalOutputs,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
|
||||
* 官方文档解释了如何在组件外部使用 Pinia Store:
|
||||
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
|
||||
*/
|
||||
export function useAgentsStoreHook() {
|
||||
return useAgentsStore(store)
|
||||
}
|
||||
40
frontend/src/stores/modules/config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { readConfig } from '@/utils/readJson.ts'
|
||||
import { store } from '@/stores'
|
||||
|
||||
export interface Config {
|
||||
title: string
|
||||
subTitle: string
|
||||
centerTitle: string
|
||||
taskPromptWords: string[]
|
||||
agentRepository: {
|
||||
storageVersionIdentifier: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const config = ref<Config>({} as Config)
|
||||
|
||||
// 异步调用readConfig
|
||||
async function initConfig() {
|
||||
config.value = await readConfig<Config>('config.json')
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
config,
|
||||
initConfig
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* 用于在组件外部(如在Pinia Store 中)使用 Pinia 提供的 store 实例。
|
||||
* 官方文档解释了如何在组件外部使用 Pinia Store:
|
||||
* https://pinia.vuejs.org/core-concepts/outside-component-usage.html#using-a-store-outside-of-a-component
|
||||
*/
|
||||
export function useConfigStoreHook() {
|
||||
return useConfigStore(store)
|
||||
}
|
||||
27
frontend/src/styles/element/index.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
|
||||
$colors: (
|
||||
'primary': (
|
||||
'base': #165DFF,
|
||||
),
|
||||
'success': (
|
||||
'base': #52C41A,
|
||||
),
|
||||
'warning': (
|
||||
'base': #FAAD14,
|
||||
),
|
||||
'danger': (
|
||||
'base': #FF4D4F,
|
||||
),
|
||||
),
|
||||
|
||||
$card: (
|
||||
'border-radius': 0.75rem,
|
||||
),
|
||||
|
||||
$text-color: (
|
||||
'secondary': #86909c,
|
||||
'regular': #1d2129,
|
||||
)
|
||||
);
|
||||
|
||||