Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46e3cd9744 | |||
| 5fb3db662b | |||
| f7a6235012 | |||
| 87b798322e | |||
| ec286b3480 | |||
| 35733f0e33 | |||
| 3c7b02b9f8 | |||
| f2030a616f | |||
| b3b5d77990 | |||
| 1da106af7d | |||
| cd2edd6966 | |||
| 22e830ff93 | |||
| c266ae7b50 | |||
| a543b75cf7 | |||
| f213987f9a | |||
| 2c623bb577 | |||
| 3353906e6f | |||
| 17716347e2 | |||
| 9ad66a67a3 | |||
| ba3aa3ccdb | |||
| 2b38eaafba | |||
| 7d5b9071e7 | |||
| 9e83f1d88c | |||
| 7e35d7c344 | |||
| 5ec40c8b83 | |||
| 948a80fc49 | |||
| dfd6fadedf |
@@ -5,8 +5,9 @@ mod validation;
|
|||||||
mod wire;
|
mod wire;
|
||||||
|
|
||||||
pub(crate) use model::{
|
pub(crate) use model::{
|
||||||
DirectCodexUserContentPart, DirectCodexUserItem, DirectCodexUserMessageEnvelope,
|
DirectCodexUserAttachmentReferencePart, DirectCodexUserContentPart, DirectCodexUserItem,
|
||||||
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
|
DirectCodexUserMessageEnvelope, DirectCodexUserMessageItem, DirectCodexUserRole,
|
||||||
|
DirectCodexUserRuntimeRegionPart,
|
||||||
};
|
};
|
||||||
pub(crate) use validation::validate_direct_codex_user_item;
|
pub(crate) use validation::validate_direct_codex_user_item;
|
||||||
pub(crate) use wire::{
|
pub(crate) use wire::{
|
||||||
|
|||||||
@@ -36,6 +36,23 @@ pub(crate) enum DirectCodexUserContentPart {
|
|||||||
AgcResourceReference { resource_id: String },
|
AgcResourceReference { resource_id: String },
|
||||||
#[serde(rename = "agc_runtime_region_reference")]
|
#[serde(rename = "agc_runtime_region_reference")]
|
||||||
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
AgcRuntimeRegionReference(DirectCodexUserRuntimeRegionPart),
|
||||||
|
/// Uploaded project attachment kept inline in canonical content.
|
||||||
|
#[serde(rename = "agc_attachment_reference")]
|
||||||
|
AgcAttachmentReference(DirectCodexUserAttachmentReferencePart),
|
||||||
|
#[serde(rename = "agc_image_reference")]
|
||||||
|
AgcImageReference(DirectCodexUserAttachmentReferencePart),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
|
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/project-workspace/generated/"))]
|
||||||
|
pub(crate) struct DirectCodexUserAttachmentReferencePart {
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) media_type: String,
|
||||||
|
#[ts(type = "number")]
|
||||||
|
pub(crate) size: u64,
|
||||||
|
pub(crate) local_path: String,
|
||||||
|
pub(crate) status: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||||
|
|||||||
@@ -40,6 +40,19 @@ pub(crate) fn validate_direct_codex_user_item(
|
|||||||
reference_count = reference_count.saturating_add(1);
|
reference_count = reference_count.saturating_add(1);
|
||||||
validate_runtime_region_reference(&manifest, reference)?;
|
validate_runtime_region_reference(&manifest, reference)?;
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcAttachmentReference(reference)
|
||||||
|
| DirectCodexUserContentPart::AgcImageReference(reference) => {
|
||||||
|
if reference.name.trim().is_empty() {
|
||||||
|
return Err("附件缺少文件名".to_string());
|
||||||
|
}
|
||||||
|
if !reference.local_path.trim().is_empty() {
|
||||||
|
sanitize_attachment_local_path(&reference.local_path)
|
||||||
|
.ok_or_else(|| "附件项目路径无效".to_string())?;
|
||||||
|
}
|
||||||
|
if !matches!(reference.status.trim(), "imported" | "failed") {
|
||||||
|
return Err("附件状态无效".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
if reference_count > MAX_DIRECT_CODEX_REFERENCES {
|
||||||
|
|||||||
@@ -100,6 +100,21 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
summary.push(']');
|
summary.push(']');
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcAttachmentReference(reference)
|
||||||
|
| DirectCodexUserContentPart::AgcImageReference(reference) => {
|
||||||
|
let mut summary = format!(
|
||||||
|
"[附件:名称={};类型={};大小={} 字节",
|
||||||
|
reference.name.trim(),
|
||||||
|
reference.media_type.trim(),
|
||||||
|
reference.size
|
||||||
|
);
|
||||||
|
if !reference.local_path.trim().is_empty() {
|
||||||
|
summary.push_str(&format!(";项目路径={}", reference.local_path.trim()));
|
||||||
|
}
|
||||||
|
summary.push_str(&format!(";状态={}", reference.status.trim()));
|
||||||
|
summary.push(']');
|
||||||
|
summary
|
||||||
|
}
|
||||||
};
|
};
|
||||||
input.push(serde_json::json!({ "type": "text", "text": text }));
|
input.push(serde_json::json!({ "type": "text", "text": text }));
|
||||||
}
|
}
|
||||||
@@ -174,4 +189,28 @@ mod tests {
|
|||||||
.expect_err("history item without type must fail");
|
.expect_err("history item without type must fail");
|
||||||
assert!(error.contains("缺少 type"), "{error}");
|
assert!(error.contains("缺少 type"), "{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn attachment_and_image_parts_remain_in_canonical_order_when_projected() {
|
||||||
|
let root = tempfile::tempdir().expect("temp project");
|
||||||
|
crate::init_local_game_project_at(root.path(), "wire-test", "wire 投影测试")
|
||||||
|
.expect("init project");
|
||||||
|
let item = json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"id": "turn-1:user",
|
||||||
|
"content": [
|
||||||
|
{"type": "input_text", "text": "先看"},
|
||||||
|
{"type": "agc_image_reference", "name": "hero.png", "mediaType": "image/png", "size": 12, "localPath": "assets/hero.png", "status": "imported"},
|
||||||
|
{"type": "agc_attachment_reference", "name": "notes.txt", "mediaType": "text/plain", "size": 4, "localPath": "assets/notes.txt", "status": "imported"}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let projected = direct_codex_user_item_to_response_item(root.path(), &item)
|
||||||
|
.expect("user response item should project");
|
||||||
|
let content = projected["content"].as_array().expect("content array");
|
||||||
|
assert_eq!(content.len(), 3);
|
||||||
|
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
|
||||||
|
assert!(content[1]["text"].as_str().unwrap().contains("hero.png"));
|
||||||
|
assert!(content[2]["text"].as_str().unwrap().contains("notes.txt"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ pub(crate) fn normalize_direct_client_turn_id(
|
|||||||
pub(crate) async fn chat_with_game_creator_direct_codex(
|
pub(crate) async fn chat_with_game_creator_direct_codex(
|
||||||
project_path: String,
|
project_path: String,
|
||||||
prompt: String,
|
prompt: String,
|
||||||
mut user_item: DirectCodexUserItem,
|
user_item: DirectCodexUserItem,
|
||||||
creation_type: Option<String>,
|
creation_type: Option<String>,
|
||||||
client_turn_id: Option<String>,
|
client_turn_id: Option<String>,
|
||||||
attachments: Option<Vec<DirectCodexTurnAttachment>>,
|
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let root = Path::new(project_path.trim());
|
let root = Path::new(project_path.trim());
|
||||||
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
let turn_id = normalize_direct_client_turn_id(client_turn_id.as_deref())?;
|
||||||
@@ -43,24 +42,7 @@ pub(crate) async fn chat_with_game_creator_direct_codex(
|
|||||||
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
redact_agent_runtime_error(root, &format!("恢复上一轮陶泥儿整包事务失败:{error}"), 500)
|
||||||
})?;
|
})?;
|
||||||
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
let turn_emitter = DirectGameCreatorTurnUpdateEmitter::new(root, turn_id.clone());
|
||||||
let mut audit = DirectCodexTurnAudit::start(
|
let mut audit = DirectCodexTurnAudit::start(root, &turn_id, &prompt, &[]);
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
&prompt,
|
|
||||||
attachments.as_deref().unwrap_or_default(),
|
|
||||||
);
|
|
||||||
let attachments = attachments.unwrap_or_default();
|
|
||||||
if !attachments.is_empty() {
|
|
||||||
let attachment_context =
|
|
||||||
render_direct_codex_user_prompt("", &attachments).map_err(|error| {
|
|
||||||
audit.finish(false);
|
|
||||||
error
|
|
||||||
})?;
|
|
||||||
let DirectCodexUserItem::Message(message) = &mut user_item;
|
|
||||||
message.content.push(DirectCodexUserContentPart::InputText {
|
|
||||||
text: attachment_context,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
validate_direct_codex_user_item(root, &user_item).map_err(|error| {
|
||||||
audit.finish(false);
|
audit.finish(false);
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -891,16 +891,9 @@ export function App({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [chatInput, setChatInput] = useState(() =>
|
|
||||||
supervisorChatOnly && initialProjectPath
|
|
||||||
? readSupervisorChatDraft(initialProjectPath)
|
|
||||||
: '',
|
|
||||||
);
|
|
||||||
const [chatReferences, setChatReferences] = useState<ChatReference[]>([]);
|
|
||||||
/**
|
/**
|
||||||
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
|
* 输入盒待发送附件(direct-codex 回合附件):上传成功后先生成 chip,随下次提交一起
|
||||||
* 交给 `chat_with_game_creator_direct_codex` 的 `attachments`。附件只存在于前端状态,
|
* 转换为 canonical user item 的 `content[]`。附件只存在于前端待发状态,提交后即清空。
|
||||||
* 提交后即清空——后端协议不变。
|
|
||||||
*/
|
*/
|
||||||
const [chatAttachments, setChatAttachments] = useState<
|
const [chatAttachments, setChatAttachments] = useState<
|
||||||
DirectCodexTurnAttachment[]
|
DirectCodexTurnAttachment[]
|
||||||
@@ -914,9 +907,6 @@ export function App({
|
|||||||
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
|
const [directCodexTurnCancelling, setDirectCodexTurnCancelling] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const queuedChatTurnSequenceRef = useRef(0);
|
const queuedChatTurnSequenceRef = useRef(0);
|
||||||
const [chatContent, setChatContent] = useState<DirectCodexUserContentPart[]>(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
const chatComposerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
||||||
/**
|
/**
|
||||||
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
|
* 切项目即清空只属于上一个项目的输入盒状态:待发附件的 `localPath` 是**项目相对**的,
|
||||||
@@ -924,7 +914,6 @@ export function App({
|
|||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
setChatContent([]);
|
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
setChatComposerNotice('');
|
setChatComposerNotice('');
|
||||||
setChatTurnQueue([]);
|
setChatTurnQueue([]);
|
||||||
@@ -3090,8 +3079,9 @@ export function App({
|
|||||||
if (!supervisorChatOnly) {
|
if (!supervisorChatOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
persistSupervisorChatDraft(initialProjectPath, chatInput);
|
const draft = chatComposerRef.current?.getDraft();
|
||||||
}, [chatInput, initialProjectPath, supervisorChatOnly]);
|
persistSupervisorChatDraft(initialProjectPath, draft?.text ?? '');
|
||||||
|
}, [initialProjectPath, supervisorChatOnly]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latestMessagesRef.current = messages;
|
latestMessagesRef.current = messages;
|
||||||
@@ -4381,10 +4371,11 @@ export function App({
|
|||||||
setWorkspaceProjectKind(projectKind);
|
setWorkspaceProjectKind(projectKind);
|
||||||
setLocalProject(openedProject);
|
setLocalProject(openedProject);
|
||||||
if (supervisorChatOnly) {
|
if (supervisorChatOnly) {
|
||||||
setChatInput(readSupervisorChatDraft(openedProject.projectPath));
|
chatComposerRef.current?.replaceText(
|
||||||
|
readSupervisorChatDraft(openedProject.projectPath),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
setChatReferences([]);
|
clearChatComposer();
|
||||||
setChatContent([]);
|
|
||||||
setManifest(openedProject.manifest);
|
setManifest(openedProject.manifest);
|
||||||
setProjectFiles([]);
|
setProjectFiles([]);
|
||||||
setProjectCheckpoints([]);
|
setProjectCheckpoints([]);
|
||||||
@@ -4623,16 +4614,28 @@ export function App({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareChatCommandDraft(commandDraft: string) {
|
function prepareChatCommandDraft(commandDraft: string) {
|
||||||
setChatInput(commandDraft);
|
chatComposerRef.current?.replaceText(commandDraft);
|
||||||
setChatReferences([]);
|
|
||||||
setChatContent([]);
|
|
||||||
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
window.setTimeout(() => chatInputRef.current?.focus(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readChatComposerDraft(): ChatComposerDraft {
|
||||||
|
return (
|
||||||
|
chatComposerRef.current?.getDraft() ?? {
|
||||||
|
text: '',
|
||||||
|
references: [],
|
||||||
|
content: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearChatComposer() {
|
||||||
|
chatComposerRef.current?.clear();
|
||||||
|
}
|
||||||
|
|
||||||
function handleChatComposerChange(draft: ChatComposerDraft) {
|
function handleChatComposerChange(draft: ChatComposerDraft) {
|
||||||
setChatInput(draft.text);
|
if (supervisorChatOnly) {
|
||||||
setChatReferences(draft.references);
|
persistSupervisorChatDraft(initialProjectPath, draft.text);
|
||||||
setChatContent(draft.content ?? []);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -5660,8 +5663,9 @@ export function App({
|
|||||||
|
|
||||||
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
async function handleChatSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const prompt = chatInput.trim();
|
const draft = readChatComposerDraft();
|
||||||
const references = chatReferences;
|
const prompt = draft.text.trim();
|
||||||
|
const references = draft.references;
|
||||||
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
if (agentRuntimeNeedsUserInput(projectSupervisorRuntimeRef.current)) {
|
||||||
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
setProjectSupervisorRuntimeError('请先回答项目总控 Agent 当前的澄清问题');
|
||||||
return;
|
return;
|
||||||
@@ -5670,9 +5674,7 @@ export function App({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setChatReferences([]);
|
|
||||||
setChatContent([]);
|
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -6762,7 +6764,7 @@ export function App({
|
|||||||
|
|
||||||
const clientTurnId = createDirectCodexConversationTurnId();
|
const clientTurnId = createDirectCodexConversationTurnId();
|
||||||
const userItem = chatComposerDraftToDirectCodexUserItem(
|
const userItem = chatComposerDraftToDirectCodexUserItem(
|
||||||
{ text: prompt, references, content: chatContent },
|
draft,
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
);
|
);
|
||||||
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
|
void executeChatAgentReply({ prompt, references, userItem, clientTurnId });
|
||||||
@@ -7032,12 +7034,32 @@ export function App({
|
|||||||
if (directProjectPath && directProjectId && directInvoke) {
|
if (directProjectPath && directProjectId && directInvoke) {
|
||||||
const clientTurnId =
|
const clientTurnId =
|
||||||
directConversationTurnId ?? createDirectCodexConversationTurnId();
|
directConversationTurnId ?? createDirectCodexConversationTurnId();
|
||||||
const effectiveUserItem =
|
const baseUserItem =
|
||||||
userItem ??
|
userItem ??
|
||||||
chatComposerDraftToDirectCodexUserItem(
|
chatComposerDraftToDirectCodexUserItem(
|
||||||
{ text: prompt, references: references ?? [], content: [] },
|
{ text: prompt, references: references ?? [], content: [] },
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
);
|
);
|
||||||
|
const effectiveUserItem = attachments?.length
|
||||||
|
? {
|
||||||
|
...baseUserItem,
|
||||||
|
content: [
|
||||||
|
...baseUserItem.content,
|
||||||
|
...attachments.map((attachment) => ({
|
||||||
|
type: attachment.mediaType.toLowerCase().startsWith('image/')
|
||||||
|
? ('agc_image_reference' as const)
|
||||||
|
: ('agc_attachment_reference' as const),
|
||||||
|
name: attachment.name,
|
||||||
|
mediaType: attachment.mediaType,
|
||||||
|
size: attachment.size ?? 0,
|
||||||
|
localPath: attachment.localPath ?? '',
|
||||||
|
status:
|
||||||
|
attachment.status ??
|
||||||
|
(attachment.localPath ? 'imported' : 'failed'),
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: baseUserItem;
|
||||||
if (
|
if (
|
||||||
!directPolicyChecked &&
|
!directPolicyChecked &&
|
||||||
projectConversationWriteConfirmedRef.current !== directProjectPath
|
projectConversationWriteConfirmedRef.current !== directProjectPath
|
||||||
@@ -7178,7 +7200,6 @@ export function App({
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
clientTurnId: string;
|
clientTurnId: string;
|
||||||
creationType?: HomeCreationType;
|
creationType?: HomeCreationType;
|
||||||
attachments?: DirectCodexTurnAttachment[];
|
|
||||||
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
userItem: ReturnType<typeof chatComposerDraftToDirectCodexUserItem>;
|
||||||
} = {
|
} = {
|
||||||
projectPath: directProjectPath,
|
projectPath: directProjectPath,
|
||||||
@@ -7189,9 +7210,6 @@ export function App({
|
|||||||
if (creationType) {
|
if (creationType) {
|
||||||
directTurnInput.creationType = creationType;
|
directTurnInput.creationType = creationType;
|
||||||
}
|
}
|
||||||
if (attachments?.length) {
|
|
||||||
directTurnInput.attachments = attachments;
|
|
||||||
}
|
|
||||||
directTurnInput.userItem = effectiveUserItem;
|
directTurnInput.userItem = effectiveUserItem;
|
||||||
const reply = await withDirectCodexSessionRefresh(() => {
|
const reply = await withDirectCodexSessionRefresh(() => {
|
||||||
// 每次调用都会新建 Rust 事件流;续期重试需重新接收同一回合的进度。
|
// 每次调用都会新建 Rust 事件流;续期重试需重新接收同一回合的进度。
|
||||||
@@ -12592,6 +12610,20 @@ export function App({
|
|||||||
content?: DirectCodexUserContentPart[];
|
content?: DirectCodexUserContentPart[];
|
||||||
}) {
|
}) {
|
||||||
const clientTurnId = createDirectCodexConversationTurnId();
|
const clientTurnId = createDirectCodexConversationTurnId();
|
||||||
|
const attachmentContent: DirectCodexUserContentPart[] = (
|
||||||
|
input.attachments ?? []
|
||||||
|
).map((attachment) => ({
|
||||||
|
type: attachment.mediaType.toLowerCase().startsWith('image/')
|
||||||
|
? ('agc_image_reference' as const)
|
||||||
|
: ('agc_attachment_reference' as const),
|
||||||
|
name: attachment.name,
|
||||||
|
mediaType: attachment.mediaType,
|
||||||
|
size: attachment.size ?? 0,
|
||||||
|
localPath: attachment.localPath ?? '',
|
||||||
|
status:
|
||||||
|
attachment.status ?? (attachment.localPath ? 'imported' : 'failed'),
|
||||||
|
}));
|
||||||
|
const content = [...(input.content ?? []), ...attachmentContent];
|
||||||
supervisorChatShouldFollowLatestRef.current = true;
|
supervisorChatShouldFollowLatestRef.current = true;
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
@@ -12606,16 +12638,18 @@ export function App({
|
|||||||
void executeChatAgentReply({
|
void executeChatAgentReply({
|
||||||
prompt: input.prompt,
|
prompt: input.prompt,
|
||||||
clientTurnId,
|
clientTurnId,
|
||||||
attachments: input.attachments?.length ? input.attachments : undefined,
|
|
||||||
references: input.references,
|
references: input.references,
|
||||||
userItem: chatComposerDraftToDirectCodexUserItem(
|
userItem: chatComposerDraftToDirectCodexUserItem(
|
||||||
{
|
{
|
||||||
text: input.prompt,
|
text: input.prompt,
|
||||||
references: input.references ?? [],
|
references: input.references ?? [],
|
||||||
content: input.content ?? [],
|
content,
|
||||||
},
|
},
|
||||||
directCodexConversationMessageId(clientTurnId, 'user'),
|
directCodexConversationMessageId(clientTurnId, 'user'),
|
||||||
),
|
),
|
||||||
|
// DirectProject 的附件已经是 canonical content part;不能再作为 sidecar
|
||||||
|
// 传给 Rust,否则会重复追加。
|
||||||
|
attachments: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12784,8 +12818,10 @@ export function App({
|
|||||||
event: FormEvent<HTMLFormElement>,
|
event: FormEvent<HTMLFormElement>,
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const prompt = chatInput.trim();
|
const draft = readChatComposerDraft();
|
||||||
const references = chatReferences;
|
const prompt = draft.text.trim();
|
||||||
|
const references = draft.references;
|
||||||
|
const content = draft.content ?? [];
|
||||||
const pendingAttachments = chatAttachments;
|
const pendingAttachments = chatAttachments;
|
||||||
if (
|
if (
|
||||||
!directCodexProductRuntime &&
|
!directCodexProductRuntime &&
|
||||||
@@ -12805,7 +12841,7 @@ export function App({
|
|||||||
if (
|
if (
|
||||||
!prompt &&
|
!prompt &&
|
||||||
references.length === 0 &&
|
references.length === 0 &&
|
||||||
chatContent.length === 0 &&
|
content.length === 0 &&
|
||||||
pendingAttachments.length === 0
|
pendingAttachments.length === 0
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -12818,12 +12854,10 @@ export function App({
|
|||||||
prompt,
|
prompt,
|
||||||
attachments: pendingAttachments,
|
attachments: pendingAttachments,
|
||||||
references,
|
references,
|
||||||
content: chatContent,
|
content,
|
||||||
});
|
});
|
||||||
if (enqueued) {
|
if (enqueued) {
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setChatContent([]);
|
|
||||||
setChatReferences([]);
|
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
}
|
}
|
||||||
@@ -12835,9 +12869,7 @@ export function App({
|
|||||||
if (!nextProjectPath) {
|
if (!nextProjectPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setChatReferences([]);
|
|
||||||
setChatContent([]);
|
|
||||||
void loadProjectConversation(nextProjectPath, false, 'replace');
|
void loadProjectConversation(nextProjectPath, false, 'replace');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -12847,7 +12879,7 @@ export function App({
|
|||||||
}
|
}
|
||||||
supervisorChatShouldFollowLatestRef.current = true;
|
supervisorChatShouldFollowLatestRef.current = true;
|
||||||
const clientTurnId = createAgentChatRunId('planning-v2-turn');
|
const clientTurnId = createAgentChatRunId('planning-v2-turn');
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -12866,23 +12898,19 @@ export function App({
|
|||||||
}
|
}
|
||||||
if (directCodexProductRuntime) {
|
if (directCodexProductRuntime) {
|
||||||
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
|
// 待发附件随本轮提交一次性交给回合;提交后清空,避免同一批附件重复挂到下一轮。
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setChatReferences([]);
|
|
||||||
setChatAttachments([]);
|
setChatAttachments([]);
|
||||||
setChatContent([]);
|
|
||||||
setChatAttachmentNotice('');
|
setChatAttachmentNotice('');
|
||||||
setChatComposerNotice('');
|
setChatComposerNotice('');
|
||||||
startDirectCodexConversationTurn({
|
startDirectCodexConversationTurn({
|
||||||
prompt,
|
prompt,
|
||||||
attachments: pendingAttachments,
|
attachments: pendingAttachments,
|
||||||
references,
|
references,
|
||||||
content: chatContent,
|
content,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setChatInput('');
|
clearChatComposer();
|
||||||
setChatReferences([]);
|
|
||||||
setChatContent([]);
|
|
||||||
setMessages((current) => [
|
setMessages((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
@@ -12910,8 +12938,6 @@ export function App({
|
|||||||
<SupervisorChatOnlyView
|
<SupervisorChatOnlyView
|
||||||
activeVersionId={chatActiveVersionId}
|
activeVersionId={chatActiveVersionId}
|
||||||
chatAgentBusy={chatAgentBusy}
|
chatAgentBusy={chatAgentBusy}
|
||||||
chatInput={chatInput}
|
|
||||||
chatReferences={chatReferences}
|
|
||||||
composerRef={chatComposerRef}
|
composerRef={chatComposerRef}
|
||||||
chatProjectAssets={chatProjectAssets}
|
chatProjectAssets={chatProjectAssets}
|
||||||
messagesRef={supervisorChatMessagesRef}
|
messagesRef={supervisorChatMessagesRef}
|
||||||
@@ -12960,8 +12986,6 @@ export function App({
|
|||||||
activeVersionId={chatActiveVersionId}
|
activeVersionId={chatActiveVersionId}
|
||||||
attachments={chatAttachments}
|
attachments={chatAttachments}
|
||||||
attachmentNotice={chatAttachmentNotice}
|
attachmentNotice={chatAttachmentNotice}
|
||||||
chatInput={chatInput}
|
|
||||||
chatReferences={chatReferences}
|
|
||||||
composerNotice={chatComposerNotice}
|
composerNotice={chatComposerNotice}
|
||||||
onCancelQueuedTurn={cancelQueuedChatTurn}
|
onCancelQueuedTurn={cancelQueuedChatTurn}
|
||||||
onCancelTurn={() => void handleCancelDirectCodexTurn()}
|
onCancelTurn={() => void handleCancelDirectCodexTurn()}
|
||||||
@@ -13181,8 +13205,6 @@ export function App({
|
|||||||
}
|
}
|
||||||
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
cancelUiCommandConfirmation={cancelUiCommandConfirmation}
|
||||||
chatAgentBusy={chatAgentBusy}
|
chatAgentBusy={chatAgentBusy}
|
||||||
chatInput={chatInput}
|
|
||||||
chatReferences={chatReferences}
|
|
||||||
composerRef={chatComposerRef}
|
composerRef={chatComposerRef}
|
||||||
chatProjectAssets={chatProjectAssets}
|
chatProjectAssets={chatProjectAssets}
|
||||||
chatInputRef={chatInputRef}
|
chatInputRef={chatInputRef}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||||
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
||||||
|
import {
|
||||||
|
COMMAND_PRIORITY_HIGH,
|
||||||
|
type EditorState,
|
||||||
|
KEY_ENTER_COMMAND,
|
||||||
|
type Klass,
|
||||||
|
type LexicalNode,
|
||||||
|
} from 'lexical';
|
||||||
|
import type { ReactElement, ReactNode, Ref } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
type RichTextInputProps = {
|
||||||
|
namespace: string;
|
||||||
|
nodes: Klass<LexicalNode>[];
|
||||||
|
initialEditorState?: EditorState | null;
|
||||||
|
contentEditable?: ReactElement<typeof ContentEditable>;
|
||||||
|
placeholder?: ReactElement;
|
||||||
|
containerClassName?: string;
|
||||||
|
containerRef?: Ref<HTMLDivElement>;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (editorState: EditorState) => void;
|
||||||
|
onEnter?: () => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SubmitOnEnter({ onEnter }: { onEnter?: () => void }) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onEnter) return undefined;
|
||||||
|
return editor.registerCommand(
|
||||||
|
KEY_ENTER_COMMAND,
|
||||||
|
(event) => {
|
||||||
|
if (!event || event.shiftKey || event.isComposing) return false;
|
||||||
|
event.preventDefault();
|
||||||
|
onEnter();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
COMMAND_PRIORITY_HIGH,
|
||||||
|
);
|
||||||
|
}, [editor, onEnter]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SetEditorEditable({ disabled }: { disabled: boolean }) {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editor.setEditable(!disabled);
|
||||||
|
}, [disabled, editor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RichTextInput({
|
||||||
|
namespace,
|
||||||
|
nodes,
|
||||||
|
initialEditorState,
|
||||||
|
contentEditable = <ContentEditable />,
|
||||||
|
placeholder,
|
||||||
|
containerClassName,
|
||||||
|
containerRef,
|
||||||
|
disabled = false,
|
||||||
|
onChange,
|
||||||
|
onEnter,
|
||||||
|
children,
|
||||||
|
}: RichTextInputProps) {
|
||||||
|
return (
|
||||||
|
<LexicalComposer
|
||||||
|
initialConfig={{
|
||||||
|
namespace,
|
||||||
|
nodes,
|
||||||
|
editorState: initialEditorState ?? undefined,
|
||||||
|
onError: (error) => {
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={containerClassName}
|
||||||
|
data-disabled={disabled ? 'true' : undefined}
|
||||||
|
>
|
||||||
|
<RichTextPlugin
|
||||||
|
contentEditable={contentEditable}
|
||||||
|
placeholder={placeholder}
|
||||||
|
ErrorBoundary={LexicalErrorBoundary}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
<SetEditorEditable disabled={disabled} />
|
||||||
|
<SubmitOnEnter onEnter={onEnter} />
|
||||||
|
{onChange ? <OnChangePlugin onChange={onChange} /> : null}
|
||||||
|
</div>
|
||||||
|
</LexicalComposer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
import { ToolCallGroup } from './ToolCallGroup';
|
import { ToolCallGroup } from './ToolCallGroup';
|
||||||
import {
|
import {
|
||||||
formatClockTime,
|
formatClockTime,
|
||||||
@@ -242,8 +242,6 @@ type ProjectSupervisorViewProps = RuntimePanelProps & {
|
|||||||
attachments?: DirectCodexTurnAttachment[];
|
attachments?: DirectCodexTurnAttachment[];
|
||||||
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
/** 上传/校验附件的提示文案(失败与成功都用它,空串不渲染)。 */
|
||||||
attachmentNotice?: string;
|
attachmentNotice?: string;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
composerRef?: RefObject<ResourceReferenceInputHandle | null>;
|
||||||
directCodex?: boolean;
|
directCodex?: boolean;
|
||||||
@@ -327,8 +325,6 @@ export function ProjectSupervisorView({
|
|||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
attachments = [],
|
attachments = [],
|
||||||
attachmentNotice = '',
|
attachmentNotice = '',
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
directCodex = false,
|
directCodex = false,
|
||||||
@@ -916,8 +912,6 @@ export function ProjectSupervisorView({
|
|||||||
Boolean(designView?.session.pendingClarification)
|
Boolean(designView?.session.pendingClarification)
|
||||||
}
|
}
|
||||||
rows={3}
|
rows={3}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
showTriggerButton={!directCodex}
|
showTriggerButton={!directCodex}
|
||||||
placeholder={
|
placeholder={
|
||||||
directCodex
|
directCodex
|
||||||
|
|||||||
+1
-7
@@ -55,7 +55,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
|
|
||||||
type ProjectWorkspaceChatPaneProps = {
|
type ProjectWorkspaceChatPaneProps = {
|
||||||
activeVersionId?: string | null;
|
activeVersionId?: string | null;
|
||||||
@@ -65,8 +65,6 @@ type ProjectWorkspaceChatPaneProps = {
|
|||||||
cancelProjectCreateInNonEmptyFolder: () => void;
|
cancelProjectCreateInNonEmptyFolder: () => void;
|
||||||
cancelUiCommandConfirmation: () => void;
|
cancelUiCommandConfirmation: () => void;
|
||||||
chatAgentBusy: boolean;
|
chatAgentBusy: boolean;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: Ref<ResourceReferenceInputHandle>;
|
composerRef?: Ref<ResourceReferenceInputHandle>;
|
||||||
chatInputRef: RefObject<HTMLDivElement | null>;
|
chatInputRef: RefObject<HTMLDivElement | null>;
|
||||||
@@ -215,8 +213,6 @@ export function ProjectWorkspaceChatPane({
|
|||||||
cancelProjectCreateInNonEmptyFolder,
|
cancelProjectCreateInNonEmptyFolder,
|
||||||
cancelUiCommandConfirmation,
|
cancelUiCommandConfirmation,
|
||||||
chatAgentBusy,
|
chatAgentBusy,
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
chatInputRef,
|
chatInputRef,
|
||||||
@@ -962,8 +958,6 @@ export function ProjectWorkspaceChatPane({
|
|||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
disabled={chatAgentBusy || projectSupervisorNeedsUserInput}
|
||||||
multiline={false}
|
multiline={false}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源"
|
placeholder="例如:像素风横版动作小游戏,或输入 @ 选择资源"
|
||||||
onChange={onChatComposerChange}
|
onChange={onChatComposerChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+130
-127
@@ -1,9 +1,6 @@
|
|||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
||||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
||||||
import {
|
import {
|
||||||
LexicalTypeaheadMenuPlugin,
|
LexicalTypeaheadMenuPlugin,
|
||||||
MenuOption,
|
MenuOption,
|
||||||
@@ -48,7 +45,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { createPortal, flushSync } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { PlatformResourceFilterBar } from '../../../../../packages/shared/src/components/PlatformResourceFilterBar';
|
import { PlatformResourceFilterBar } from '../../../../../packages/shared/src/components/PlatformResourceFilterBar';
|
||||||
import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs';
|
import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs';
|
||||||
@@ -58,6 +55,7 @@ import {
|
|||||||
type GameIterationVersion,
|
type GameIterationVersion,
|
||||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
import { resolveTauriInvoke } from '../../app/tauri';
|
||||||
|
import RichTextInput from '../../components/RichTextInput';
|
||||||
import {
|
import {
|
||||||
cancelLocalProjectResourcePreviewScope,
|
cancelLocalProjectResourcePreviewScope,
|
||||||
createProjectResourcePreviewRequestId,
|
createProjectResourcePreviewRequestId,
|
||||||
@@ -79,7 +77,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ChatComposerDraft,
|
type ChatComposerDraft,
|
||||||
type ChatReference,
|
type ChatReference,
|
||||||
chatReferenceListKey,
|
chatReferenceToContentPart,
|
||||||
currentIterationVersionAssets,
|
currentIterationVersionAssets,
|
||||||
dedupeChatReferences,
|
dedupeChatReferences,
|
||||||
refreshResourceReference,
|
refreshResourceReference,
|
||||||
@@ -98,9 +96,12 @@ import {
|
|||||||
import { usePromptPolish } from './usePromptPolish';
|
import { usePromptPolish } from './usePromptPolish';
|
||||||
|
|
||||||
type ResourceReferenceInputProps = {
|
type ResourceReferenceInputProps = {
|
||||||
value: string;
|
value?: EditorState | string | null;
|
||||||
references: ChatReference[];
|
/** 仅用于尚未迁移的调用方提供初始引用;不会参与后续状态同步。 */
|
||||||
onChange: (draft: ChatComposerDraft) => void;
|
references?: ChatReference[];
|
||||||
|
onChange?: (draft: ChatComposerDraft) => void;
|
||||||
|
onEditorStateChange?: (editorState: EditorState) => void;
|
||||||
|
initialDraft?: Pick<ChatComposerDraft, 'text' | 'references'>;
|
||||||
assets: GameCreationAppAssetManifestEntry[];
|
assets: GameCreationAppAssetManifestEntry[];
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
/**
|
/**
|
||||||
@@ -157,10 +158,13 @@ function createResourcePickerScopeStates(): Record<
|
|||||||
|
|
||||||
export type ResourceReferenceInputHandle = {
|
export type ResourceReferenceInputHandle = {
|
||||||
insertReferences: (references: ChatReference[]) => void;
|
insertReferences: (references: ChatReference[]) => void;
|
||||||
/** 追加纯文本(语音识别结果):写在当前光标处,且不覆盖用户已输入的内容。 */
|
|
||||||
insertText: (text: string) => void;
|
insertText: (text: string) => void;
|
||||||
openPicker: () => void;
|
openPicker: () => void;
|
||||||
focus: () => void;
|
focus: () => void;
|
||||||
|
clear: () => void;
|
||||||
|
replaceText: (text: string) => void;
|
||||||
|
/** 直接读取 Lexical 当前状态,不在宿主组件复制一份编辑器 state。 */
|
||||||
|
getDraft: () => ChatComposerDraft;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ResourceMentionOption extends MenuOption {
|
class ResourceMentionOption extends MenuOption {
|
||||||
@@ -172,15 +176,15 @@ class ResourceMentionOption extends MenuOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function referenceListKey(references: ChatReference[]) {
|
function appendInputText(content: DirectCodexUserContentPart[], text: string) {
|
||||||
return chatReferenceListKey(references);
|
const previous = content[content.length - 1];
|
||||||
}
|
if (previous?.type === 'input_text') {
|
||||||
|
previous.text += text;
|
||||||
function sameDraft(left: ChatComposerDraft, right: ChatComposerDraft) {
|
return;
|
||||||
return (
|
}
|
||||||
left.text === right.text &&
|
if (text.trim()) {
|
||||||
referenceListKey(left.references) === referenceListKey(right.references)
|
content.push({ type: 'input_text', text });
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectDraftParts(
|
function collectDraftParts(
|
||||||
@@ -192,43 +196,25 @@ function collectDraftParts(
|
|||||||
if ($isTextNode(node)) {
|
if ($isTextNode(node)) {
|
||||||
const text = node.getTextContent();
|
const text = node.getTextContent();
|
||||||
textParts.push(text);
|
textParts.push(text);
|
||||||
if (text) content.push({ type: 'input_text', text });
|
appendInputText(content, text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($isLineBreakNode(node)) {
|
if ($isLineBreakNode(node)) {
|
||||||
textParts.push('\n');
|
textParts.push('\n');
|
||||||
content.push({ type: 'input_text', text: '\n' });
|
appendInputText(content, '\n');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($isResourceReferenceNode(node)) {
|
if ($isResourceReferenceNode(node)) {
|
||||||
textParts.push(`@${node.__reference.label}`);
|
textParts.push(`@${node.__reference.label}`);
|
||||||
references.push(node.__reference);
|
references.push(node.__reference);
|
||||||
content.push(
|
content.push(chatReferenceToContentPart(node.__reference));
|
||||||
node.__reference.type === 'resource'
|
|
||||||
? {
|
|
||||||
type: 'agc_resource_reference',
|
|
||||||
resourceId: node.__reference.resourceId,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: 'agc_runtime_region_reference',
|
|
||||||
label: node.__reference.label,
|
|
||||||
runId: node.__reference.runId,
|
|
||||||
versionId: node.__reference.versionId,
|
|
||||||
elementTag: node.__reference.elementTag,
|
|
||||||
elementRole: node.__reference.elementRole,
|
|
||||||
text: node.__reference.text,
|
|
||||||
width: node.__reference.width,
|
|
||||||
height: node.__reference.height,
|
|
||||||
resourceIds: node.__reference.resourceIds,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($isElementNode(node)) {
|
if ($isElementNode(node)) {
|
||||||
node.getChildren().forEach((child, index) => {
|
node.getChildren().forEach((child, index) => {
|
||||||
if (index > 0 && node.getType() === 'root') {
|
if (index > 0 && node.getType() === 'root') {
|
||||||
textParts.push('\n');
|
textParts.push('\n');
|
||||||
content.push({ type: 'input_text', text: '\n' });
|
appendInputText(content, '\n');
|
||||||
}
|
}
|
||||||
collectDraftParts(child, textParts, references, content);
|
collectDraftParts(child, textParts, references, content);
|
||||||
});
|
});
|
||||||
@@ -248,7 +234,14 @@ function readDraftFromNodes(): ChatComposerDraft {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readDraftFromEditorState(editorState: EditorState): ChatComposerDraft {
|
// The pure projection is exported for submit-time reads and focused tests.
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export function readResourceReferenceDraft(
|
||||||
|
editorState: EditorState | null,
|
||||||
|
): ChatComposerDraft {
|
||||||
|
if (!editorState) {
|
||||||
|
return { text: '', references: [], content: [] };
|
||||||
|
}
|
||||||
return editorState.read(readDraftFromNodes);
|
return editorState.read(readDraftFromNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,13 +274,12 @@ function findDraftMentionToken(line: string, token: string, from: number) {
|
|||||||
/**
|
/**
|
||||||
* 把「文本 + 引用列表」这对外部表示切成编辑区要写的内容。
|
* 把「文本 + 引用列表」这对外部表示切成编辑区要写的内容。
|
||||||
*
|
*
|
||||||
* 不变量:切完写进编辑器后,编辑器读回来的草稿必须与 props 等价。`collectDraftParts`
|
* 不变量:写入编辑器后,编辑器读回的草稿就是唯一当前值。`collectDraftParts`
|
||||||
* 会把每个 chip 读成一段 `@显示名` 文本,所以 chip 只能内联嵌在文本里同名 token 的位置上,
|
* 会把每个 chip 读成一段 `@显示名` 文本,所以 chip 只能内联嵌在文本里同名 token 的位置上,
|
||||||
* 不能另起一段堆在末尾——否则读回来的文本每重建一轮就多一段 `@显示名`,
|
* 不能另起一段堆在末尾;编辑器本身是唯一 authority,不再通过 props 比较后反复重建。
|
||||||
* `sameDraft` 永远判定为不相等,编辑器就会一轮轮重建、文本一轮轮变长。
|
|
||||||
*
|
*
|
||||||
* 文本里已经没有对应 token 的引用(例如 AI 润色整体改写了文本)内联补到最后一段末尾:
|
* 文本里已经没有对应 token 的引用(例如 AI 润色整体改写了文本)内联补到最后一段末尾,
|
||||||
* 引用不会凭空消失,而且只补一次——下一轮 props 里就带上这个 token,重建随即收敛。
|
* 引用不会凭空消失;这只发生在一次明确的初始草稿/润色写入中。
|
||||||
*/
|
*/
|
||||||
function buildDraftSegments(
|
function buildDraftSegments(
|
||||||
value: string,
|
value: string,
|
||||||
@@ -452,30 +444,25 @@ function $staleResourceReferenceNodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ResourceReferenceEditor({
|
function ResourceReferenceEditor({
|
||||||
value,
|
|
||||||
references,
|
|
||||||
onChange,
|
onChange,
|
||||||
|
onEditorStateChange,
|
||||||
|
initialDraft,
|
||||||
assets,
|
assets,
|
||||||
projectPath,
|
projectPath,
|
||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
versions,
|
versions,
|
||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
|
||||||
ariaLabel,
|
|
||||||
multiline,
|
multiline,
|
||||||
rows,
|
|
||||||
showTriggerButton = true,
|
showTriggerButton = true,
|
||||||
showPolishAction = true,
|
showPolishAction = true,
|
||||||
inputRef,
|
|
||||||
composerRef,
|
composerRef,
|
||||||
|
rootRef,
|
||||||
}: ResourceReferenceInputProps & {
|
}: ResourceReferenceInputProps & {
|
||||||
composerRef?: Ref<ResourceReferenceInputHandle>;
|
composerRef?: Ref<ResourceReferenceInputHandle>;
|
||||||
|
rootRef: RefObject<HTMLDivElement | null>;
|
||||||
}) {
|
}) {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const lastEmittedDraftRef = useRef<ChatComposerDraft>({
|
const skipInitialDraftChangeRef = useRef(false);
|
||||||
text: '',
|
|
||||||
references: [],
|
|
||||||
});
|
|
||||||
const [query, setQuery] = useState<string | null>(null);
|
const [query, setQuery] = useState<string | null>(null);
|
||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
// Enter 提交要避开候选浮层。用 ref 让命令处理器读到最新状态,不必因为
|
// Enter 提交要避开候选浮层。用 ref 让命令处理器读到最新状态,不必因为
|
||||||
@@ -498,8 +485,6 @@ function ResourceReferenceEditor({
|
|||||||
bottom: number;
|
bottom: number;
|
||||||
width: number;
|
width: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const assetsContentSignature = assetsSignature(assets);
|
const assetsContentSignature = assetsSignature(assets);
|
||||||
const versionsContentSignature = iterationsSignature(versions);
|
const versionsContentSignature = iterationsSignature(versions);
|
||||||
const assetReferences = useMemo(
|
const assetReferences = useMemo(
|
||||||
@@ -604,7 +589,6 @@ function ResourceReferenceEditor({
|
|||||||
if (!insert.trim()) return;
|
if (!insert.trim()) return;
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
let selection = $getSelection();
|
let selection = $getSelection();
|
||||||
// 选区失效(跨会话恢复草稿后常见)时回落到草稿末尾,与 `insertReferences` 同口径。
|
|
||||||
if (
|
if (
|
||||||
!$isRangeSelection(selection) ||
|
!$isRangeSelection(selection) ||
|
||||||
!selection.anchor.getNode().isAttached()
|
!selection.anchor.getNode().isAttached()
|
||||||
@@ -613,11 +597,8 @@ function ResourceReferenceEditor({
|
|||||||
selection = $getSelection();
|
selection = $getSelection();
|
||||||
}
|
}
|
||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
// 认领的光标处的已有内容保持不变:这里只插入,不删除任何节点。
|
|
||||||
const rootText = $getRoot().getTextContent();
|
const rootText = $getRoot().getTextContent();
|
||||||
if (rootText && !/\s$/u.test(rootText)) {
|
if (rootText && !/\s$/u.test(rootText)) selection.insertText(' ');
|
||||||
selection.insertText(' ');
|
|
||||||
}
|
|
||||||
selection.insertText(insert);
|
selection.insertText(insert);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -633,6 +614,20 @@ function ResourceReferenceEditor({
|
|||||||
insertText,
|
insertText,
|
||||||
openPicker,
|
openPicker,
|
||||||
focus: () => editor.focus(),
|
focus: () => editor.focus(),
|
||||||
|
clear: () => {
|
||||||
|
editor.update(() => {
|
||||||
|
applyDraftToRoot('', []);
|
||||||
|
$getRoot().selectEnd();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
replaceText: (text: string) => {
|
||||||
|
editor.update(() => {
|
||||||
|
const current = readDraftFromNodes();
|
||||||
|
applyDraftToRoot(text, current.references);
|
||||||
|
$getRoot().selectEnd();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getDraft: () => readResourceReferenceDraft(editor.getEditorState()),
|
||||||
}),
|
}),
|
||||||
[editor, insertReferences, insertText, openPicker],
|
[editor, insertReferences, insertText, openPicker],
|
||||||
);
|
);
|
||||||
@@ -641,24 +636,18 @@ function ResourceReferenceEditor({
|
|||||||
editor.setEditable(!disabled);
|
editor.setEditable(!disabled);
|
||||||
}, [disabled, editor]);
|
}, [disabled, editor]);
|
||||||
|
|
||||||
|
const initialDraftAppliedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextDraft: ChatComposerDraft = {
|
if (initialDraftAppliedRef.current || !initialDraft) {
|
||||||
text: value,
|
|
||||||
references,
|
|
||||||
};
|
|
||||||
if (sameDraft(lastEmittedDraftRef.current, nextDraft)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
initialDraftAppliedRef.current = true;
|
||||||
|
skipInitialDraftChangeRef.current = true;
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
applyDraftToRoot(value, references);
|
applyDraftToRoot(initialDraft.text, initialDraft.references);
|
||||||
// 立刻按编辑器自己的口径读一遍刚写进去的内容,并记为「已同步草稿」:
|
|
||||||
// `OnChangePlugin` 稍后读到的就是这一份,两边一致才不会触发下一轮重建。
|
|
||||||
lastEmittedDraftRef.current = readDraftFromNodes();
|
|
||||||
// 程序化重建草稿(切会话 / 重开会话恢复草稿)后把光标收回草稿末尾:
|
|
||||||
// 既保证恢复后光标落在文本末尾,也保证后续 @ 引用按顺序追加而不是插到旧位置。
|
|
||||||
$getRoot().selectEnd();
|
$getRoot().selectEnd();
|
||||||
});
|
});
|
||||||
}, [editor, references, value]);
|
}, [editor, initialDraft]);
|
||||||
|
|
||||||
const assetsById = useMemo(
|
const assetsById = useMemo(
|
||||||
() => new Map(assets.map((asset) => [asset.id, asset])),
|
() => new Map(assets.map((asset) => [asset.id, asset])),
|
||||||
@@ -769,18 +758,22 @@ function ResourceReferenceEditor({
|
|||||||
);
|
);
|
||||||
const acknowledgedDraftKeyRef = useRef<string | null>(null);
|
const acknowledgedDraftKeyRef = useRef<string | null>(null);
|
||||||
// 拦截表单提交需要读到最新草稿,用 ref 保存本次渲染的草稿与派生值,避免闭包读到旧值。
|
// 拦截表单提交需要读到最新草稿,用 ref 保存本次渲染的草稿与派生值,避免闭包读到旧值。
|
||||||
const liveDraftRef = useRef<ChatComposerDraft>({ text: value, references });
|
const liveDraftRef = useRef<ChatComposerDraft>({
|
||||||
liveDraftRef.current = { text: value, references };
|
text: initialDraft?.text ?? '',
|
||||||
|
references: initialDraft?.references ?? [],
|
||||||
|
content: [],
|
||||||
|
});
|
||||||
const reminderDisabledRef = useRef(reminderDisabled);
|
const reminderDisabledRef = useRef(reminderDisabled);
|
||||||
reminderDisabledRef.current = reminderDisabled;
|
reminderDisabledRef.current = reminderDisabled;
|
||||||
|
|
||||||
const applyPromptText = useCallback(
|
const applyPromptText = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
flushSync(() => {
|
editor.update(() => {
|
||||||
onChange({ text, references: liveDraftRef.current.references });
|
applyDraftToRoot(text, liveDraftRef.current.references);
|
||||||
|
$getRoot().selectEnd();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onChange],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
const readPromptText = useCallback(() => liveDraftRef.current.text, []);
|
const readPromptText = useCallback(() => liveDraftRef.current.text, []);
|
||||||
@@ -819,7 +812,7 @@ function ResourceReferenceEditor({
|
|||||||
setReminderOpen(false);
|
setReminderOpen(false);
|
||||||
acknowledgedDraftKeyRef.current = chatPromptDraftKey(liveDraftRef.current);
|
acknowledgedDraftKeyRef.current = chatPromptDraftKey(liveDraftRef.current);
|
||||||
rootRef.current?.closest('form')?.requestSubmit();
|
rootRef.current?.closest('form')?.requestSubmit();
|
||||||
}, []);
|
}, [rootRef]);
|
||||||
|
|
||||||
const useOriginalAndSubmit = useCallback(() => {
|
const useOriginalAndSubmit = useCallback(() => {
|
||||||
clearPolishError();
|
clearPolishError();
|
||||||
@@ -867,14 +860,15 @@ function ResourceReferenceEditor({
|
|||||||
};
|
};
|
||||||
form.addEventListener('submit', handleFormSubmit, true);
|
form.addEventListener('submit', handleFormSubmit, true);
|
||||||
return () => form.removeEventListener('submit', handleFormSubmit, true);
|
return () => form.removeEventListener('submit', handleFormSubmit, true);
|
||||||
}, []);
|
}, [rootRef]);
|
||||||
|
|
||||||
// 草稿发出去或被清空后重新开始一轮:清掉润色结果与「本轮已确认」标记。
|
// 草稿发出去或被清空后重新开始一轮:清掉润色结果与「本轮已确认」标记。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value.trim() !== '' || references.length > 0) return;
|
const draft = liveDraftRef.current;
|
||||||
|
if (draft.text.trim() !== '' || draft.references.length > 0) return;
|
||||||
resetPromptPolish();
|
resetPromptPolish();
|
||||||
acknowledgedDraftKeyRef.current = null;
|
acknowledgedDraftKeyRef.current = null;
|
||||||
}, [references, resetPromptPolish, value]);
|
}, [resetPromptPolish]);
|
||||||
|
|
||||||
const renderMentionMenu: MenuRenderFn<ResourceMentionOption> = useCallback(
|
const renderMentionMenu: MenuRenderFn<ResourceMentionOption> = useCallback(
|
||||||
(_anchorElementRef, itemProps) => {
|
(_anchorElementRef, itemProps) => {
|
||||||
@@ -942,7 +936,7 @@ function ResourceReferenceEditor({
|
|||||||
document.body,
|
document.body,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[],
|
[rootRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
const pickerReferences = useMemo(() => {
|
const pickerReferences = useMemo(() => {
|
||||||
@@ -973,7 +967,7 @@ function ResourceReferenceEditor({
|
|||||||
bottom: Math.max(12, window.innerHeight - rect.top + 8),
|
bottom: Math.max(12, window.innerHeight - rect.top + 8),
|
||||||
width,
|
width,
|
||||||
});
|
});
|
||||||
}, []);
|
}, [rootRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pickerOpen) {
|
if (!pickerOpen) {
|
||||||
@@ -990,31 +984,7 @@ function ResourceReferenceEditor({
|
|||||||
}, [pickerOpen, updatePickerPosition]);
|
}, [pickerOpen, updatePickerPosition]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
ref={rootRef}
|
|
||||||
className={`resource-reference-input${multiline ? '' : ' is-single-line'}`}
|
|
||||||
data-disabled={disabled ? 'true' : undefined}
|
|
||||||
>
|
|
||||||
<RichTextPlugin
|
|
||||||
contentEditable={
|
|
||||||
<ContentEditable
|
|
||||||
ref={inputRef}
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
className="resource-reference-input-editor"
|
|
||||||
style={
|
|
||||||
multiline && rows
|
|
||||||
? { minHeight: `${Math.max(72, rows * 22)}px` }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
<span className="resource-reference-input-placeholder">
|
|
||||||
{placeholder}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
|
||||||
/>
|
|
||||||
<div className="resource-reference-input-actions">
|
<div className="resource-reference-input-actions">
|
||||||
{showTriggerButton ? (
|
{showTriggerButton ? (
|
||||||
<button
|
<button
|
||||||
@@ -1037,7 +1007,9 @@ function ResourceReferenceEditor({
|
|||||||
aria-label="AI 润色"
|
aria-label="AI 润色"
|
||||||
title="AI 润色"
|
title="AI 润色"
|
||||||
aria-busy={polishing}
|
aria-busy={polishing}
|
||||||
disabled={disabled || polishing || value.trim() === ''}
|
disabled={
|
||||||
|
disabled || polishing || liveDraftRef.current.text.trim() === ''
|
||||||
|
}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onClick={() => void polishPrompt()}
|
onClick={() => void polishPrompt()}
|
||||||
>
|
>
|
||||||
@@ -1241,15 +1213,17 @@ function ResourceReferenceEditor({
|
|||||||
) : null}
|
) : null}
|
||||||
<OnChangePlugin
|
<OnChangePlugin
|
||||||
onChange={(editorState) => {
|
onChange={(editorState) => {
|
||||||
const nextDraft = readDraftFromEditorState(editorState);
|
const nextDraft = readResourceReferenceDraft(editorState);
|
||||||
if (sameDraft(lastEmittedDraftRef.current, nextDraft)) {
|
liveDraftRef.current = nextDraft;
|
||||||
|
if (skipInitialDraftChangeRef.current) {
|
||||||
|
skipInitialDraftChangeRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastEmittedDraftRef.current = nextDraft;
|
onEditorStateChange?.(editorState);
|
||||||
onChange(nextDraft);
|
onChange?.(nextDraft);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1321,17 +1295,46 @@ export const ResourceReferenceInput = forwardRef<
|
|||||||
ResourceReferenceInputHandle,
|
ResourceReferenceInputHandle,
|
||||||
ResourceReferenceInputProps
|
ResourceReferenceInputProps
|
||||||
>(function ResourceReferenceInput(props, ref) {
|
>(function ResourceReferenceInput(props, ref) {
|
||||||
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const initialEditorState =
|
||||||
|
typeof props.value === 'string' ? null : props.value;
|
||||||
|
const initialDraft =
|
||||||
|
props.initialDraft ??
|
||||||
|
(typeof props.value === 'string'
|
||||||
|
? { text: props.value, references: props.references ?? [] }
|
||||||
|
: undefined);
|
||||||
return (
|
return (
|
||||||
<LexicalComposer
|
<RichTextInput
|
||||||
initialConfig={{
|
namespace="agc-resource-reference-input"
|
||||||
namespace: 'agc-resource-reference-input',
|
nodes={[ResourceReferenceNode]}
|
||||||
nodes: [ResourceReferenceNode],
|
initialEditorState={initialEditorState}
|
||||||
onError: (error) => {
|
containerRef={rootRef}
|
||||||
throw error;
|
containerClassName={`resource-reference-input${props.multiline ? '' : ' is-single-line'}`}
|
||||||
},
|
disabled={props.disabled}
|
||||||
}}
|
contentEditable={
|
||||||
|
<ContentEditable
|
||||||
|
aria-label={props.ariaLabel}
|
||||||
|
className="resource-reference-input-editor"
|
||||||
|
ref={props.inputRef}
|
||||||
|
style={
|
||||||
|
props.multiline && props.rows
|
||||||
|
? { minHeight: `${Math.max(72, props.rows * 22)}px` }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
<span className="resource-reference-input-placeholder">
|
||||||
|
{props.placeholder}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ResourceReferenceEditor {...props} composerRef={ref} />
|
<ResourceReferenceEditor
|
||||||
</LexicalComposer>
|
{...props}
|
||||||
|
initialDraft={initialDraft}
|
||||||
|
composerRef={ref}
|
||||||
|
rootRef={rootRef}
|
||||||
|
/>
|
||||||
|
</RichTextInput>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-7
@@ -33,7 +33,7 @@ import {
|
|||||||
ResourceReferenceInput,
|
ResourceReferenceInput,
|
||||||
type ResourceReferenceInputHandle,
|
type ResourceReferenceInputHandle,
|
||||||
} from './ResourceReferenceInput';
|
} from './ResourceReferenceInput';
|
||||||
import type { ChatComposerDraft, ChatReference } from './resourceReferences';
|
import type { ChatComposerDraft } from './resourceReferences';
|
||||||
|
|
||||||
type RuntimeControlProps = ComponentProps<
|
type RuntimeControlProps = ComponentProps<
|
||||||
typeof ProjectSupervisorRuntimeControls
|
typeof ProjectSupervisorRuntimeControls
|
||||||
@@ -44,8 +44,6 @@ const CHAT_SCROLL_BOTTOM_THRESHOLD = 24;
|
|||||||
type SupervisorChatOnlyViewProps = {
|
type SupervisorChatOnlyViewProps = {
|
||||||
activeVersionId?: string | null;
|
activeVersionId?: string | null;
|
||||||
chatAgentBusy: boolean;
|
chatAgentBusy: boolean;
|
||||||
chatInput: string;
|
|
||||||
chatReferences: ChatReference[];
|
|
||||||
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
chatProjectAssets: import('../../../../../packages/shared/src/contracts/gameCreationApp').GameCreationAppAssetManifestEntry[];
|
||||||
composerRef?: Ref<ResourceReferenceInputHandle>;
|
composerRef?: Ref<ResourceReferenceInputHandle>;
|
||||||
directCodex?: boolean;
|
directCodex?: boolean;
|
||||||
@@ -82,8 +80,6 @@ type SupervisorChatOnlyViewProps = {
|
|||||||
export function SupervisorChatOnlyView({
|
export function SupervisorChatOnlyView({
|
||||||
activeVersionId = null,
|
activeVersionId = null,
|
||||||
chatAgentBusy,
|
chatAgentBusy,
|
||||||
chatInput,
|
|
||||||
chatReferences,
|
|
||||||
chatProjectAssets,
|
chatProjectAssets,
|
||||||
composerRef,
|
composerRef,
|
||||||
directCodex = false,
|
directCodex = false,
|
||||||
@@ -328,8 +324,6 @@ export function SupervisorChatOnlyView({
|
|||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
disabled={chatAgentBusy || needsUserInput}
|
disabled={chatAgentBusy || needsUserInput}
|
||||||
rows={3}
|
rows={3}
|
||||||
value={chatInput}
|
|
||||||
references={chatReferences}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
directCodex ? '描述你的想法' : '给项目总控 Agent 发消息'
|
directCodex ? '描述你的想法' : '给项目总控 Agent 发消息'
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
|
export type DirectCodexUserAttachmentReferencePart = {
|
||||||
|
name: string;
|
||||||
|
mediaType: string;
|
||||||
|
size: number;
|
||||||
|
localPath: string;
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
+7
-3
@@ -1,5 +1,5 @@
|
|||||||
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DirectCodexUserAttachmentReferencePart } from './DirectCodexUserAttachmentReferencePart';
|
||||||
import type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
import type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
||||||
|
|
||||||
export type DirectCodexUserContentPart =
|
export type DirectCodexUserContentPart =
|
||||||
@@ -7,4 +7,8 @@ export type DirectCodexUserContentPart =
|
|||||||
| { type: 'agc_resource_reference'; resourceId: string }
|
| { type: 'agc_resource_reference'; resourceId: string }
|
||||||
| ({
|
| ({
|
||||||
type: 'agc_runtime_region_reference';
|
type: 'agc_runtime_region_reference';
|
||||||
} & DirectCodexUserRuntimeRegionPart);
|
} & DirectCodexUserRuntimeRegionPart)
|
||||||
|
| ({
|
||||||
|
type: 'agc_attachment_reference';
|
||||||
|
} & DirectCodexUserAttachmentReferencePart)
|
||||||
|
| ({ type: 'agc_image_reference' } & DirectCodexUserAttachmentReferencePart);
|
||||||
|
|||||||
+7
-3
@@ -1,5 +1,9 @@
|
|||||||
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
import type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
import type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
||||||
|
|
||||||
export type DirectCodexUserItem = DirectCodexUserMessageItem;
|
/**
|
||||||
|
* DirectProject 本轮 user input 的唯一结构化入口。
|
||||||
|
*/
|
||||||
|
export type DirectCodexUserItem = {
|
||||||
|
type: 'message';
|
||||||
|
} & DirectCodexUserMessageItem;
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
import type { DirectCodexUserItem } from './DirectCodexUserItem';
|
||||||
|
|
||||||
|
export type DirectCodexUserMessageEnvelope = { item: DirectCodexUserItem };
|
||||||
+2
-4
@@ -1,11 +1,9 @@
|
|||||||
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
import type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
import type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
||||||
import type { DirectCodexUserRole } from './DirectCodexUserRole';
|
import type { DirectCodexUserRole } from './DirectCodexUserRole';
|
||||||
|
|
||||||
export type DirectCodexUserMessageItem = {
|
export type DirectCodexUserMessageItem = {
|
||||||
type: 'message';
|
|
||||||
role: DirectCodexUserRole;
|
role: DirectCodexUserRole;
|
||||||
content: DirectCodexUserContentPart[];
|
content: Array<DirectCodexUserContentPart>;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type DirectCodexUserRole = 'user';
|
export type DirectCodexUserRole = 'user';
|
||||||
|
|||||||
+9
-9
@@ -1,13 +1,13 @@
|
|||||||
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type DirectCodexUserRuntimeRegionPart = {
|
export type DirectCodexUserRuntimeRegionPart = {
|
||||||
label: string;
|
label: string;
|
||||||
runId?: string;
|
runId: string | null;
|
||||||
versionId?: string;
|
versionId: string | null;
|
||||||
elementTag?: string;
|
elementTag: string | null;
|
||||||
elementRole?: string;
|
elementRole: string | null;
|
||||||
text?: string;
|
text: string | null;
|
||||||
width?: number;
|
width: number | null;
|
||||||
height?: number;
|
height: number | null;
|
||||||
resourceIds: string[];
|
resourceIds: Array<string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
export type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
export type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
|
||||||
|
export type { DirectCodexUserAttachmentReferencePart } from './DirectCodexUserAttachmentReferencePart';
|
||||||
export type { DirectCodexUserItem } from './DirectCodexUserItem';
|
export type { DirectCodexUserItem } from './DirectCodexUserItem';
|
||||||
export type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
export type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
|
||||||
export type { DirectCodexUserRole } from './DirectCodexUserRole';
|
export type { DirectCodexUserRole } from './DirectCodexUserRole';
|
||||||
export type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
export type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
|
||||||
|
export type { DirectCodexUserMessageEnvelope } from './DirectCodexUserMessageEnvelope';
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
DirectCodexUserContentPart,
|
DirectCodexUserContentPart,
|
||||||
DirectCodexUserItem,
|
DirectCodexUserItem,
|
||||||
DirectCodexUserMessageItem,
|
|
||||||
} from './generated';
|
} from './generated';
|
||||||
|
|
||||||
export type ResourceReferenceSource =
|
export type ResourceReferenceSource =
|
||||||
@@ -52,8 +51,8 @@ export type ChatReference = ResourceReference | RuntimeRegionReference;
|
|||||||
export type ChatComposerDraft = {
|
export type ChatComposerDraft = {
|
||||||
text: string;
|
text: string;
|
||||||
references: ChatReference[];
|
references: ChatReference[];
|
||||||
/** Lexical 顺序对应的 canonical user content;仅由编辑器读回时提供。 */
|
/** Lexical 顺序对应的 canonical user content;只从 EditorState 派生。 */
|
||||||
content?: DirectCodexUserContentPart[];
|
content: DirectCodexUserContentPart[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert';
|
export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert';
|
||||||
@@ -103,13 +102,13 @@ export function chatReferenceToContentPart(
|
|||||||
return {
|
return {
|
||||||
type: 'agc_runtime_region_reference',
|
type: 'agc_runtime_region_reference',
|
||||||
label: reference.label,
|
label: reference.label,
|
||||||
runId: reference.runId,
|
runId: reference.runId ?? null,
|
||||||
versionId: reference.versionId,
|
versionId: reference.versionId ?? null,
|
||||||
elementTag: reference.elementTag,
|
elementTag: reference.elementTag ?? null,
|
||||||
elementRole: reference.elementRole,
|
elementRole: reference.elementRole ?? null,
|
||||||
text: reference.text,
|
text: reference.text ?? null,
|
||||||
width: reference.width,
|
width: reference.width ?? null,
|
||||||
height: reference.height,
|
height: reference.height ?? null,
|
||||||
resourceIds: reference.resourceIds,
|
resourceIds: reference.resourceIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -118,24 +117,15 @@ export function chatComposerDraftToDirectCodexUserItem(
|
|||||||
draft: ChatComposerDraft,
|
draft: ChatComposerDraft,
|
||||||
id: string,
|
id: string,
|
||||||
): DirectCodexUserItem {
|
): DirectCodexUserItem {
|
||||||
const content = draft.content?.length
|
const content = draft.content.filter(
|
||||||
? draft.content
|
(part) => part.type !== 'input_text' || part.text.trim().length > 0,
|
||||||
: draft.references.length > 0
|
);
|
||||||
? [
|
|
||||||
...(draft.text
|
|
||||||
? [{ type: 'input_text' as const, text: draft.text }]
|
|
||||||
: []),
|
|
||||||
...draft.references.map(chatReferenceToContentPart),
|
|
||||||
]
|
|
||||||
: draft.text
|
|
||||||
? [{ type: 'input_text' as const, text: draft.text }]
|
|
||||||
: [];
|
|
||||||
return {
|
return {
|
||||||
type: 'message',
|
type: 'message',
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content,
|
content,
|
||||||
id,
|
id,
|
||||||
} satisfies DirectCodexUserMessageItem;
|
} satisfies DirectCodexUserItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resourceDisplayName(asset: GameCreationAppAssetManifestEntry) {
|
export function resourceDisplayName(asset: GameCreationAppAssetManifestEntry) {
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
||||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
||||||
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
||||||
import { readImage, readText } from '@tauri-apps/plugin-clipboard-manager';
|
import { readImage, readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
@@ -13,13 +9,13 @@ import {
|
|||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
COMMAND_PRIORITY_HIGH,
|
COMMAND_PRIORITY_HIGH,
|
||||||
createCommand,
|
createCommand,
|
||||||
KEY_ENTER_COMMAND,
|
|
||||||
type LexicalCommand,
|
type LexicalCommand,
|
||||||
PASTE_COMMAND,
|
PASTE_COMMAND,
|
||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import { Upload } from 'lucide-react';
|
import { Upload } from 'lucide-react';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import RichTextInput from '../../../../components/RichTextInput';
|
||||||
import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore';
|
import type { Draft, HomeAttachmentDraft } from '../../useHomeDraftStore';
|
||||||
import { $createAttachmentNode, AttachmentNode } from './attachmentNode';
|
import { $createAttachmentNode, AttachmentNode } from './attachmentNode';
|
||||||
|
|
||||||
@@ -101,29 +97,9 @@ function selectEditableEndWhenNeeded() {
|
|||||||
root.selectEnd();
|
root.selectEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorPlugins({
|
function EditorPlugins() {
|
||||||
onChange,
|
|
||||||
onEnter,
|
|
||||||
}: Pick<RichInputAreaProps, 'onChange' | 'onEnter'>) {
|
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() =>
|
|
||||||
editor.registerCommand(
|
|
||||||
KEY_ENTER_COMMAND,
|
|
||||||
(event) => {
|
|
||||||
if (!event || event.shiftKey || event.isComposing) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
onEnter();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
COMMAND_PRIORITY_HIGH,
|
|
||||||
),
|
|
||||||
[editor, onEnter],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
@@ -188,13 +164,7 @@ function EditorPlugins({
|
|||||||
[editor],
|
[editor],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<OnChangePlugin
|
|
||||||
onChange={(editorState) => {
|
|
||||||
onChange(editorState);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UploadButton() {
|
export function UploadButton() {
|
||||||
@@ -229,34 +199,27 @@ export function UploadButton() {
|
|||||||
|
|
||||||
export default function RichInputArea(props: RichInputAreaProps) {
|
export default function RichInputArea(props: RichInputAreaProps) {
|
||||||
return (
|
return (
|
||||||
<LexicalComposer
|
<RichTextInput
|
||||||
initialConfig={{
|
namespace="home-rich-input"
|
||||||
namespace: 'home-rich-input',
|
nodes={[AttachmentNode]}
|
||||||
nodes: [AttachmentNode],
|
initialEditorState={props.value}
|
||||||
editorState: props.value ?? undefined,
|
onChange={props.onChange}
|
||||||
onError: (error) => {
|
onEnter={props.onEnter}
|
||||||
throw error;
|
containerClassName="relative grid min-h-9 gap-2"
|
||||||
},
|
contentEditable={
|
||||||
}}
|
<ContentEditable
|
||||||
>
|
aria-label="创作想法"
|
||||||
<div className="relative grid min-h-9 gap-2">
|
className="min-h-9 w-full whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 text-[13px] text-(--platform-text-strong) outline-0"
|
||||||
<RichTextPlugin
|
|
||||||
contentEditable={
|
|
||||||
<ContentEditable
|
|
||||||
aria-label="创作想法"
|
|
||||||
className="min-h-9 w-full whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 text-[13px] text-(--platform-text-strong) outline-0"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
<span className="pointer-events-none absolute inset-x-0 top-0 text-[13px] text-(--platform-text-muted)">
|
|
||||||
{props.placeholder}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
ErrorBoundary={LexicalErrorBoundary}
|
|
||||||
/>
|
/>
|
||||||
{props.children}
|
}
|
||||||
</div>
|
placeholder={
|
||||||
<EditorPlugins onChange={props.onChange} onEnter={props.onEnter} />
|
<span className="pointer-events-none absolute inset-x-0 top-0 text-[13px] text-(--platform-text-muted)">
|
||||||
</LexicalComposer>
|
{props.placeholder}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EditorPlugins />
|
||||||
|
{props.children}
|
||||||
|
</RichTextInput>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ import {
|
|||||||
import { ResourceReferenceInput } from '../../features/project-workspace/ResourceReferenceInput';
|
import { ResourceReferenceInput } from '../../features/project-workspace/ResourceReferenceInput';
|
||||||
import {
|
import {
|
||||||
type ChatComposerDraft,
|
type ChatComposerDraft,
|
||||||
type ChatReference,
|
|
||||||
dispatchResourceReferenceInsert,
|
dispatchResourceReferenceInsert,
|
||||||
isResourceReferenceOverlayTarget,
|
isResourceReferenceOverlayTarget,
|
||||||
resolveActiveIterationVersion,
|
resolveActiveIterationVersion,
|
||||||
@@ -1563,9 +1562,6 @@ export default function ProjectDevelopmentView({
|
|||||||
* 的提示词与聊天 `@` 逐字同源,不存在第二种引用格式。
|
* 的提示词与聊天 `@` 逐字同源,不存在第二种引用格式。
|
||||||
* 引用列表按面板生命周期重开:每次打开面板清空一份,面板关闭后不再被读取。
|
* 引用列表按面板生命周期重开:每次打开面板清空一份,面板关闭后不再被读取。
|
||||||
*/
|
*/
|
||||||
const [quickEditReferences, setQuickEditReferences] = useState<
|
|
||||||
ChatReference[]
|
|
||||||
>([]);
|
|
||||||
/**
|
/**
|
||||||
* 「生成动画」的源与浮层。
|
* 「生成动画」的源与浮层。
|
||||||
*
|
*
|
||||||
@@ -6470,7 +6466,6 @@ export default function ProjectDevelopmentView({
|
|||||||
setQuickEditSourceLayer(layer);
|
setQuickEditSourceLayer(layer);
|
||||||
const panelDraft = createResourceQuickEditPanelDraft(layer);
|
const panelDraft = createResourceQuickEditPanelDraft(layer);
|
||||||
setQuickEditPanel(panelDraft);
|
setQuickEditPanel(panelDraft);
|
||||||
setQuickEditReferences([]);
|
|
||||||
resourceQuickEditRequestRef.current = {
|
resourceQuickEditRequestRef.current = {
|
||||||
...createResourceEditRequestIdentity(panelDraft.prompt),
|
...createResourceEditRequestIdentity(panelDraft.prompt),
|
||||||
sourceLayerId: layer.id,
|
sourceLayerId: layer.id,
|
||||||
@@ -6510,7 +6505,6 @@ export default function ProjectDevelopmentView({
|
|||||||
*/
|
*/
|
||||||
const applyResourceQuickEditDraft = useCallback(
|
const applyResourceQuickEditDraft = useCallback(
|
||||||
(draft: ChatComposerDraft) => {
|
(draft: ChatComposerDraft) => {
|
||||||
setQuickEditReferences(draft.references);
|
|
||||||
applyResourceQuickEditPrompt(draft.text);
|
applyResourceQuickEditPrompt(draft.text);
|
||||||
},
|
},
|
||||||
[applyResourceQuickEditPrompt],
|
[applyResourceQuickEditPrompt],
|
||||||
@@ -8128,9 +8122,12 @@ export default function ProjectDevelopmentView({
|
|||||||
// 由它拼装,因此出站 payload 与聊天 `@` 一致。
|
// 由它拼装,因此出站 payload 与聊天 `@` 一致。
|
||||||
<div className="resource-canvas-quick-edit-prompt-input">
|
<div className="resource-canvas-quick-edit-prompt-input">
|
||||||
<ResourceReferenceInput
|
<ResourceReferenceInput
|
||||||
|
key={quickEditSourceLayer?.id}
|
||||||
ariaLabel="快速编辑提示词"
|
ariaLabel="快速编辑提示词"
|
||||||
value={quickEditPanel.prompt}
|
initialDraft={{
|
||||||
references={quickEditReferences}
|
text: quickEditPanel.prompt,
|
||||||
|
references: [],
|
||||||
|
}}
|
||||||
onChange={applyResourceQuickEditDraft}
|
onChange={applyResourceQuickEditDraft}
|
||||||
assets={manifest.assets}
|
assets={manifest.assets}
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
|
// @vitest-environment-options {"url":"http://localhost"}
|
||||||
import {
|
import {
|
||||||
cleanup,
|
cleanup,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
within,
|
within,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { useState } from 'react';
|
import { useRef } from 'react';
|
||||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
shouldRemindChatPromptPolish,
|
shouldRemindChatPromptPolish,
|
||||||
writeChatPromptPolishReminderDisabled,
|
writeChatPromptPolishReminderDisabled,
|
||||||
} from '../src/features/project-workspace/chatPromptPolish';
|
} from '../src/features/project-workspace/chatPromptPolish';
|
||||||
|
import type { ResourceReferenceInputHandle } from '../src/features/project-workspace/ResourceReferenceInput';
|
||||||
import { ResourceReferenceInput } from '../src/features/project-workspace/ResourceReferenceInput';
|
import { ResourceReferenceInput } from '../src/features/project-workspace/ResourceReferenceInput';
|
||||||
import {
|
import {
|
||||||
type ChatComposerDraft,
|
type ChatComposerDraft,
|
||||||
@@ -27,6 +29,17 @@ import {
|
|||||||
resourceReferenceFromAsset,
|
resourceReferenceFromAsset,
|
||||||
} from '../src/features/project-workspace/resourceReferences';
|
} from '../src/features/project-workspace/resourceReferences';
|
||||||
|
|
||||||
|
const storage = new Map<string, string>();
|
||||||
|
Object.defineProperty(window, 'localStorage', {
|
||||||
|
configurable: true,
|
||||||
|
value: {
|
||||||
|
clear: () => storage.clear(),
|
||||||
|
getItem: (key: string) => storage.get(key) ?? null,
|
||||||
|
removeItem: (key: string) => storage.delete(key),
|
||||||
|
setItem: (key: string, value: string) => storage.set(key, String(value)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
type TauriInvoke = (
|
type TauriInvoke = (
|
||||||
command: string,
|
command: string,
|
||||||
args?: Record<string, unknown>,
|
args?: Record<string, unknown>,
|
||||||
@@ -78,21 +91,30 @@ function ControlledChatComposer({
|
|||||||
initialReferences?: ChatReference[];
|
initialReferences?: ChatReference[];
|
||||||
onSubmitDraft: (draft: ChatComposerDraft) => void;
|
onSubmitDraft: (draft: ChatComposerDraft) => void;
|
||||||
}) {
|
}) {
|
||||||
const [draft, setDraft] = useState<ChatComposerDraft>({
|
const composerRef = useRef<ResourceReferenceInputHandle | null>(null);
|
||||||
text: initialText,
|
|
||||||
references: initialReferences,
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSubmitDraft(draft);
|
const draft = composerRef.current?.getDraft();
|
||||||
|
const submitted =
|
||||||
|
draft && (draft.text || draft.references.length > 0)
|
||||||
|
? draft
|
||||||
|
: {
|
||||||
|
text: initialText,
|
||||||
|
references: initialReferences,
|
||||||
|
content: [],
|
||||||
|
};
|
||||||
|
onSubmitDraft({
|
||||||
|
text: submitted.text,
|
||||||
|
references: submitted.references,
|
||||||
|
} as ChatComposerDraft);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ResourceReferenceInput
|
<ResourceReferenceInput
|
||||||
value={draft.text}
|
ref={composerRef}
|
||||||
references={draft.references}
|
initialDraft={{ text: initialText, references: initialReferences }}
|
||||||
onChange={setDraft}
|
onChange={() => {}}
|
||||||
assets={[]}
|
assets={[]}
|
||||||
projectPath="C:/project"
|
projectPath="C:/project"
|
||||||
ariaLabel="创作想法"
|
ariaLabel="创作想法"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { $getRoot } from 'lexical';
|
import { $getRoot } from 'lexical';
|
||||||
import { StrictMode, useState } from 'react';
|
import { createRef, StrictMode, useState } from 'react';
|
||||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -20,7 +20,10 @@ import {
|
|||||||
LOCAL_GAME_PREVIEW_INSPECT_MESSAGE,
|
LOCAL_GAME_PREVIEW_INSPECT_MESSAGE,
|
||||||
parseLocalGamePreviewInspectMessage,
|
parseLocalGamePreviewInspectMessage,
|
||||||
} from '../src/features/project-workspace/LocalGamePreviewFrame';
|
} from '../src/features/project-workspace/LocalGamePreviewFrame';
|
||||||
import { ResourceReferenceInput } from '../src/features/project-workspace/ResourceReferenceInput';
|
import {
|
||||||
|
ResourceReferenceInput,
|
||||||
|
type ResourceReferenceInputHandle,
|
||||||
|
} from '../src/features/project-workspace/ResourceReferenceInput';
|
||||||
import {
|
import {
|
||||||
type ChatComposerDraft,
|
type ChatComposerDraft,
|
||||||
type ChatReference,
|
type ChatReference,
|
||||||
@@ -193,27 +196,20 @@ describe('ResourceReferenceInput', () => {
|
|||||||
const onChange = vi.fn<(draft: ChatComposerDraft) => void>();
|
const onChange = vi.fn<(draft: ChatComposerDraft) => void>();
|
||||||
const reference = resourceReferenceFromAsset(assets[0]!, 'asset-picker');
|
const reference = resourceReferenceFromAsset(assets[0]!, 'asset-picker');
|
||||||
function Controlled() {
|
function Controlled() {
|
||||||
const [draft, setDraft] = useState<ChatComposerDraft>({
|
const composerRef = createRef<ResourceReferenceInputHandle>();
|
||||||
text: '原始需求',
|
|
||||||
references: [reference],
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() => composerRef.current?.replaceText('润色后的需求')}
|
||||||
setDraft((current) => ({ ...current, text: '润色后的需求' }))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
模拟润色
|
模拟润色
|
||||||
</button>
|
</button>
|
||||||
<ResourceReferenceInput
|
<ResourceReferenceInput
|
||||||
value={draft.text}
|
ref={composerRef}
|
||||||
references={draft.references}
|
value={null}
|
||||||
onChange={(next) => {
|
initialDraft={{ text: '原始需求', references: [reference] }}
|
||||||
onChange(next);
|
onChange={onChange}
|
||||||
setDraft(next);
|
|
||||||
}}
|
|
||||||
assets={assets}
|
assets={assets}
|
||||||
projectPath="C:/project"
|
projectPath="C:/project"
|
||||||
ariaLabel="聊天"
|
ariaLabel="聊天"
|
||||||
@@ -228,10 +224,9 @@ describe('ResourceReferenceInput', () => {
|
|||||||
await settleComposer();
|
await settleComposer();
|
||||||
await settleComposer();
|
await settleComposer();
|
||||||
|
|
||||||
// 重建必须收敛,而且不许把「重建后的编辑器内容」当成一次用户编辑回抛给宿主:
|
// 这次替换是对唯一 EditorState 的明确编辑动作,只产生一次派生快照,
|
||||||
// 旧实现用 props 覆写 lastEmittedDraftRef,读回来的文本里多出的 `@显示名`
|
// 不会因为 props 回写而重复触发。
|
||||||
// 会触发下一轮重建,文本一轮轮变长(渲染循环)。
|
expect(onChange).toHaveBeenCalledTimes(1);
|
||||||
expect(onChange).not.toHaveBeenCalled();
|
|
||||||
const editorText =
|
const editorText =
|
||||||
document.querySelector('.resource-reference-input-editor')?.textContent ??
|
document.querySelector('.resource-reference-input-editor')?.textContent ??
|
||||||
'';
|
'';
|
||||||
@@ -766,10 +761,12 @@ describe('ResourceReferenceInput', () => {
|
|||||||
test('restores a cross-session draft with the caret at the end of the text', async () => {
|
test('restores a cross-session draft with the caret at the end of the text', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onChange = vi.fn<(draft: ChatComposerDraft) => void>();
|
const onChange = vi.fn<(draft: ChatComposerDraft) => void>();
|
||||||
const { rerender } = render(
|
const composerRef = createRef<ResourceReferenceInputHandle>();
|
||||||
|
render(
|
||||||
<ResourceReferenceInput
|
<ResourceReferenceInput
|
||||||
value="上一个会话的草稿"
|
value="上一个会话的草稿"
|
||||||
references={[]}
|
references={[]}
|
||||||
|
ref={composerRef}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
assets={assets}
|
assets={assets}
|
||||||
projectPath="C:/project"
|
projectPath="C:/project"
|
||||||
@@ -777,17 +774,8 @@ describe('ResourceReferenceInput', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 切换 / 重开会话:外部草稿被整体替换。
|
// 切换 / 重开会话:通过输入区 handle 替换 Lexical 唯一状态。
|
||||||
rerender(
|
composerRef.current?.replaceText('恢复出来的草稿');
|
||||||
<ResourceReferenceInput
|
|
||||||
value="恢复出来的草稿"
|
|
||||||
references={[]}
|
|
||||||
onChange={onChange}
|
|
||||||
assets={assets}
|
|
||||||
projectPath="C:/project"
|
|
||||||
ariaLabel="聊天"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
await user.click(screen.getByRole('button', { name: '插入素材引用' }));
|
await user.click(screen.getByRole('button', { name: '插入素材引用' }));
|
||||||
await user.click(screen.getByRole('option', { name: /hero/u }));
|
await user.click(screen.getByRole('option', { name: /hero/u }));
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ describe('DirectProject user Response item', () => {
|
|||||||
source: 'asset-picker',
|
source: 'asset-picker',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
content: [
|
||||||
|
{ type: 'input_text', text: '请使用素材' },
|
||||||
|
{ type: 'agc_resource_reference', resourceId: 'asset-hero' },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user