Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edfd1f9143 | |||
| 423f2d7184 | |||
| aa9076c95c | |||
| 41366dd71d | |||
| f4b496caca | |||
| fc0fd846f9 | |||
| fa99d9ad62 | |||
| 64a52005f8 | |||
| 645d806fed | |||
| 0b9d16e8f1 | |||
| d5c4e0e780 | |||
| 30e27abc60 | |||
| b607501916 | |||
| 46e3cd9744 | |||
| 5fb3db662b | |||
| 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,21 @@ 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),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,18 @@ 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) => {
|
||||||
|
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,20 @@ pub(crate) fn direct_codex_user_item_to_wire_input(
|
|||||||
summary.push(']');
|
summary.push(']');
|
||||||
summary
|
summary
|
||||||
}
|
}
|
||||||
|
DirectCodexUserContentPart::AgcAttachmentReference(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 +188,26 @@ 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_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_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(), 2);
|
||||||
|
assert!(content[0]["text"].as_str().unwrap().contains("先看"));
|
||||||
|
assert!(content[1]["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
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+210
-150
File diff suppressed because it is too large
Load Diff
+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 发消息'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,32 @@
|
|||||||
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
|
* 回合运行中用户再次发送时,消息进入 FIFO 队列而不是被丢弃;当前回合结束后按入队顺序
|
||||||
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
* 依次发出。队列项能在输入盒上方单独取消。这里只放与 React 无关的纯逻辑,便于单测。
|
||||||
*/
|
*/
|
||||||
import type { DirectCodexTurnAttachment } from '../app-shell/directCodexTurnAttachments';
|
import type { DirectCodexUserItem } from './generated';
|
||||||
import type { DirectCodexUserContentPart } from './generated';
|
import { directCodexContentToPromptText } from './resourceReferences';
|
||||||
import type { ChatReference } from './resourceReferences';
|
|
||||||
|
|
||||||
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
/** 队列上限:满了以后拒绝入队并给出可读提示,而不是静默丢消息。 */
|
||||||
export const MAX_QUEUED_CHAT_TURNS = 5;
|
export const MAX_QUEUED_CHAT_TURNS = 5;
|
||||||
|
|
||||||
export type QueuedChatTurn = {
|
export type QueuedChatTurn = {
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
clientTurnId: string;
|
||||||
attachments: DirectCodexTurnAttachment[];
|
userItem: DirectCodexUserItem;
|
||||||
references: ChatReference[];
|
|
||||||
content?: DirectCodexUserContentPart[];
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createQueuedChatTurn(input: {
|
export function createQueuedChatTurn(input: {
|
||||||
id: string;
|
id: string;
|
||||||
prompt: string;
|
clientTurnId: string;
|
||||||
attachments?: readonly DirectCodexTurnAttachment[];
|
userItem: DirectCodexUserItem;
|
||||||
references?: readonly ChatReference[];
|
|
||||||
content?: readonly DirectCodexUserContentPart[];
|
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}): QueuedChatTurn {
|
}): QueuedChatTurn {
|
||||||
return {
|
return {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
prompt: input.prompt,
|
clientTurnId: input.clientTurnId,
|
||||||
attachments: [...(input.attachments ?? [])],
|
userItem: {
|
||||||
references: [...(input.references ?? [])],
|
...input.userItem,
|
||||||
content: [...(input.content ?? [])],
|
content: [...input.userItem.content],
|
||||||
|
},
|
||||||
createdAt: input.createdAt,
|
createdAt: input.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -79,14 +75,19 @@ export function chatQueueFullNotice(): string {
|
|||||||
|
|
||||||
/** 队列 chip 上显示的文字:单行、有长度上限。 */
|
/** 队列 chip 上显示的文字:单行、有长度上限。 */
|
||||||
export function queuedChatTurnLabel(turn: QueuedChatTurn): string {
|
export function queuedChatTurnLabel(turn: QueuedChatTurn): string {
|
||||||
const text = turn.prompt.trim().replace(/\s+/gu, ' ');
|
const text = directCodexContentToPromptText(turn.userItem.content)
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/gu, ' ');
|
||||||
if (text) {
|
if (text) {
|
||||||
return text.length > 24 ? `${text.slice(0, 24)}…` : text;
|
return text.length > 24 ? `${text.slice(0, 24)}…` : text;
|
||||||
}
|
}
|
||||||
if (turn.attachments.length > 0) {
|
const attachment = turn.userItem.content.find(
|
||||||
return `附件 · ${turn.attachments[0]?.name ?? '未命名'}`;
|
(part) => part.type === 'agc_attachment_reference',
|
||||||
|
);
|
||||||
|
if (attachment?.type === 'agc_attachment_reference') {
|
||||||
|
return `附件 · ${attachment.name || '未命名'}`;
|
||||||
}
|
}
|
||||||
if (turn.references.length > 0) {
|
if (turn.userItem.content.some((part) => part.type !== 'input_text')) {
|
||||||
return '素材引用';
|
return '素材引用';
|
||||||
}
|
}
|
||||||
return '未命名消息';
|
return '未命名消息';
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { resolveTauriInvoke } from '../../app/tauri';
|
import { resolveTauriInvoke } from '../../app/tauri';
|
||||||
import {
|
import type { DirectCodexUserContentPart } from './generated';
|
||||||
type ChatComposerDraft,
|
|
||||||
chatReferenceListKey,
|
|
||||||
} from './resourceReferences';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
* 「不再提醒」偏好存本机 localStorage,不进 manifest、不进后端。
|
||||||
@@ -61,8 +58,10 @@ export function writeChatPromptPolishReminderDisabled(disabled: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 草稿指纹:用于判断「本轮草稿」是否已经被润色或确认过。 */
|
/** 草稿指纹:用于判断「本轮草稿」是否已经被润色或确认过。 */
|
||||||
export function chatPromptDraftKey(draft: ChatComposerDraft) {
|
export function chatPromptDraftKey(
|
||||||
return `${draft.text}\u0000${chatReferenceListKey(draft.references)}`;
|
content: readonly DirectCodexUserContentPart[],
|
||||||
|
) {
|
||||||
|
return JSON.stringify(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,25 +72,27 @@ export function chatPromptDraftKey(draft: ChatComposerDraft) {
|
|||||||
* 4. 草稿不是以 `/` 开头的命令 —— 命令走直通路径,不参与提醒。
|
* 4. 草稿不是以 `/` 开头的命令 —— 命令走直通路径,不参与提醒。
|
||||||
*/
|
*/
|
||||||
export function shouldRemindChatPromptPolish({
|
export function shouldRemindChatPromptPolish({
|
||||||
draft,
|
content,
|
||||||
|
prompt,
|
||||||
acknowledgedDraftKey,
|
acknowledgedDraftKey,
|
||||||
reminderDisabled,
|
reminderDisabled,
|
||||||
}: {
|
}: {
|
||||||
draft: ChatComposerDraft;
|
content: readonly DirectCodexUserContentPart[];
|
||||||
|
prompt: string;
|
||||||
acknowledgedDraftKey: string | null;
|
acknowledgedDraftKey: string | null;
|
||||||
reminderDisabled: boolean;
|
reminderDisabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (reminderDisabled) {
|
if (reminderDisabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const text = draft.text.trim();
|
const text = prompt.trim();
|
||||||
if (text.length < CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH) {
|
if (text.length < CHAT_PROMPT_POLISH_REMINDER_MIN_TEXT_LENGTH) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (text.startsWith('/')) {
|
if (text.startsWith('/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return chatPromptDraftKey(draft) !== acknowledgedDraftKey;
|
return chatPromptDraftKey(content) !== acknowledgedDraftKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+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;
|
||||||
|
};
|
||||||
+6
-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,7 @@ 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);
|
||||||
|
|||||||
+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 =
|
||||||
@@ -50,10 +49,8 @@ export type RuntimeRegionReference = {
|
|||||||
export type ChatReference = ResourceReference | RuntimeRegionReference;
|
export type ChatReference = ResourceReference | RuntimeRegionReference;
|
||||||
|
|
||||||
export type ChatComposerDraft = {
|
export type ChatComposerDraft = {
|
||||||
text: string;
|
/** Lexical 顺序对应的 canonical user content;这是唯一草稿真相。 */
|
||||||
references: ChatReference[];
|
content: DirectCodexUserContentPart[];
|
||||||
/** Lexical 顺序对应的 canonical user content;仅由编辑器读回时提供。 */
|
|
||||||
content?: DirectCodexUserContentPart[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert';
|
export const RESOURCE_REFERENCE_INSERT_EVENT = 'agc-resource-reference-insert';
|
||||||
@@ -89,11 +86,39 @@ export function isResourceReferenceOverlayTarget(target: EventTarget | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_CHAT_COMPOSER_DRAFT: ChatComposerDraft = {
|
export const EMPTY_CHAT_COMPOSER_DRAFT: ChatComposerDraft = {
|
||||||
text: '',
|
|
||||||
references: [],
|
|
||||||
content: [],
|
content: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function directCodexContentToPromptText(
|
||||||
|
content: readonly DirectCodexUserContentPart[],
|
||||||
|
assets: readonly GameCreationAppAssetManifestEntry[] = [],
|
||||||
|
) {
|
||||||
|
const labels = new Map(
|
||||||
|
assets.map((asset) => [asset.id, resourceDisplayName(asset)]),
|
||||||
|
);
|
||||||
|
return content
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === 'input_text') return part.text;
|
||||||
|
if (part.type === 'agc_resource_reference') {
|
||||||
|
return `@${labels.get(part.resourceId) ?? part.resourceId}`;
|
||||||
|
}
|
||||||
|
if (part.type === 'agc_runtime_region_reference') {
|
||||||
|
return `@${part.label}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasMeaningfulDirectCodexContent(
|
||||||
|
content: readonly DirectCodexUserContentPart[],
|
||||||
|
) {
|
||||||
|
return content.some(
|
||||||
|
(part) => part.type !== 'input_text' || part.text.trim().length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function chatReferenceToContentPart(
|
export function chatReferenceToContentPart(
|
||||||
reference: ChatReference,
|
reference: ChatReference,
|
||||||
): DirectCodexUserContentPart {
|
): DirectCodexUserContentPart {
|
||||||
@@ -103,13 +128,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 +143,19 @@ export function chatComposerDraftToDirectCodexUserItem(
|
|||||||
draft: ChatComposerDraft,
|
draft: ChatComposerDraft,
|
||||||
id: string,
|
id: string,
|
||||||
): DirectCodexUserItem {
|
): DirectCodexUserItem {
|
||||||
const content = draft.content?.length
|
|
||||||
? draft.content
|
|
||||||
: 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: [...draft.content],
|
||||||
id,
|
id,
|
||||||
} satisfies DirectCodexUserMessageItem;
|
} satisfies DirectCodexUserItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directCodexUserItemFromContent(
|
||||||
|
content: readonly DirectCodexUserContentPart[],
|
||||||
|
id: string,
|
||||||
|
): DirectCodexUserItem {
|
||||||
|
return chatComposerDraftToDirectCodexUserItem({ content: [...content] }, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,10 +105,13 @@ import {
|
|||||||
type LocalGamePreviewInspectSelection,
|
type LocalGamePreviewInspectSelection,
|
||||||
resolveEmbeddedPreviewUrl,
|
resolveEmbeddedPreviewUrl,
|
||||||
} from '../../features/project-workspace/LocalGamePreviewFrame';
|
} from '../../features/project-workspace/LocalGamePreviewFrame';
|
||||||
import { ResourceReferenceInput } from '../../features/project-workspace/ResourceReferenceInput';
|
import {
|
||||||
|
ResourceReferenceInput,
|
||||||
|
type ResourceReferenceInputHandle,
|
||||||
|
} from '../../features/project-workspace/ResourceReferenceInput';
|
||||||
import {
|
import {
|
||||||
type ChatComposerDraft,
|
type ChatComposerDraft,
|
||||||
type ChatReference,
|
directCodexContentToPromptText,
|
||||||
dispatchResourceReferenceInsert,
|
dispatchResourceReferenceInsert,
|
||||||
isResourceReferenceOverlayTarget,
|
isResourceReferenceOverlayTarget,
|
||||||
resolveActiveIterationVersion,
|
resolveActiveIterationVersion,
|
||||||
@@ -1555,6 +1558,9 @@ export default function ProjectDevelopmentView({
|
|||||||
useState<CanvasLayer | null>(null);
|
useState<CanvasLayer | null>(null);
|
||||||
const [quickEditPanel, setQuickEditPanel] =
|
const [quickEditPanel, setQuickEditPanel] =
|
||||||
useState<QuickEditPanelState | null>(null);
|
useState<QuickEditPanelState | null>(null);
|
||||||
|
const quickEditPromptInputRef = useRef<ResourceReferenceInputHandle | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
/**
|
/**
|
||||||
* 快速编辑提示词里的 `@` 资源引用。
|
* 快速编辑提示词里的 `@` 资源引用。
|
||||||
*
|
*
|
||||||
@@ -1563,9 +1569,6 @@ export default function ProjectDevelopmentView({
|
|||||||
* 的提示词与聊天 `@` 逐字同源,不存在第二种引用格式。
|
* 的提示词与聊天 `@` 逐字同源,不存在第二种引用格式。
|
||||||
* 引用列表按面板生命周期重开:每次打开面板清空一份,面板关闭后不再被读取。
|
* 引用列表按面板生命周期重开:每次打开面板清空一份,面板关闭后不再被读取。
|
||||||
*/
|
*/
|
||||||
const [quickEditReferences, setQuickEditReferences] = useState<
|
|
||||||
ChatReference[]
|
|
||||||
>([]);
|
|
||||||
/**
|
/**
|
||||||
* 「生成动画」的源与浮层。
|
* 「生成动画」的源与浮层。
|
||||||
*
|
*
|
||||||
@@ -6470,7 +6473,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,
|
||||||
@@ -6489,6 +6491,13 @@ export default function ProjectDevelopmentView({
|
|||||||
* 失败的那份请求,状态回到 idle、错误清空——同时提交侧会按新提示词重铸请求身份。
|
* 失败的那份请求,状态回到 idle、错误清空——同时提交侧会按新提示词重铸请求身份。
|
||||||
*/
|
*/
|
||||||
const applyResourceQuickEditPrompt = useCallback((text: string) => {
|
const applyResourceQuickEditPrompt = useCallback((text: string) => {
|
||||||
|
const currentDraft = quickEditPromptInputRef.current?.getDraft();
|
||||||
|
if (
|
||||||
|
currentDraft &&
|
||||||
|
directCodexContentToPromptText(currentDraft.content) !== text
|
||||||
|
) {
|
||||||
|
quickEditPromptInputRef.current?.replaceText(text);
|
||||||
|
}
|
||||||
setQuickEditPanel((current) =>
|
setQuickEditPanel((current) =>
|
||||||
current
|
current
|
||||||
? {
|
? {
|
||||||
@@ -6510,8 +6519,9 @@ export default function ProjectDevelopmentView({
|
|||||||
*/
|
*/
|
||||||
const applyResourceQuickEditDraft = useCallback(
|
const applyResourceQuickEditDraft = useCallback(
|
||||||
(draft: ChatComposerDraft) => {
|
(draft: ChatComposerDraft) => {
|
||||||
setQuickEditReferences(draft.references);
|
applyResourceQuickEditPrompt(
|
||||||
applyResourceQuickEditPrompt(draft.text);
|
directCodexContentToPromptText(draft.content),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[applyResourceQuickEditPrompt],
|
[applyResourceQuickEditPrompt],
|
||||||
);
|
);
|
||||||
@@ -8128,9 +8138,15 @@ export default function ProjectDevelopmentView({
|
|||||||
// 由它拼装,因此出站 payload 与聊天 `@` 一致。
|
// 由它拼装,因此出站 payload 与聊天 `@` 一致。
|
||||||
<div className="resource-canvas-quick-edit-prompt-input">
|
<div className="resource-canvas-quick-edit-prompt-input">
|
||||||
<ResourceReferenceInput
|
<ResourceReferenceInput
|
||||||
|
ref={quickEditPromptInputRef}
|
||||||
|
key={quickEditSourceLayer?.id}
|
||||||
ariaLabel="快速编辑提示词"
|
ariaLabel="快速编辑提示词"
|
||||||
value={quickEditPanel.prompt}
|
initialContent={[
|
||||||
references={quickEditReferences}
|
{
|
||||||
|
type: 'input_text',
|
||||||
|
text: quickEditPanel.prompt,
|
||||||
|
},
|
||||||
|
]}
|
||||||
onChange={applyResourceQuickEditDraft}
|
onChange={applyResourceQuickEditDraft}
|
||||||
assets={manifest.assets}
|
assets={manifest.assets}
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
dequeueChatTurn,
|
dequeueChatTurn,
|
||||||
enqueueChatTurn,
|
enqueueChatTurn,
|
||||||
isChatTurnQueueFull,
|
isChatTurnQueueFull,
|
||||||
|
queuedChatTurnLabel,
|
||||||
removeQueuedChatTurn,
|
removeQueuedChatTurn,
|
||||||
} from '../../src/features/project-workspace/chatComposerQueue';
|
} from '../../src/features/project-workspace/chatComposerQueue';
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
VOICE_INPUT_UNSUPPORTED_MESSAGE,
|
VOICE_INPUT_UNSUPPORTED_MESSAGE,
|
||||||
} from '../../src/features/project-workspace/chatComposerVoice';
|
} from '../../src/features/project-workspace/chatComposerVoice';
|
||||||
import { ComposerVoiceButton } from '../../src/features/project-workspace/ComposerControls';
|
import { ComposerVoiceButton } from '../../src/features/project-workspace/ComposerControls';
|
||||||
|
import { directCodexUserItemFromContent } from '../../src/features/project-workspace/resourceReferences';
|
||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
createGameCreationAppManifest,
|
createGameCreationAppManifest,
|
||||||
@@ -175,23 +177,29 @@ function installFakeSpeechRecognition(): {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 排队回合只持 canonical user item;展示文案由它派生。 */
|
||||||
|
function queuedTurn(
|
||||||
|
id: string,
|
||||||
|
clientTurnId: string,
|
||||||
|
text: string,
|
||||||
|
createdAt: number,
|
||||||
|
) {
|
||||||
|
return createQueuedChatTurn({
|
||||||
|
id,
|
||||||
|
clientTurnId,
|
||||||
|
userItem: directCodexUserItemFromContent(
|
||||||
|
[{ type: 'input_text', text }],
|
||||||
|
`${clientTurnId}:user`,
|
||||||
|
),
|
||||||
|
createdAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function registerChatComposerControlTests() {
|
export function registerChatComposerControlTests() {
|
||||||
it('keeps queued chat turns in FIFO order and drops only the cancelled one', () => {
|
it('keeps queued chat turns in FIFO order and drops only the cancelled one', () => {
|
||||||
const first = createQueuedChatTurn({
|
const first = queuedTurn('turn-1', 'client-1', '第一条', 1);
|
||||||
id: 'turn-1',
|
const second = queuedTurn('turn-2', 'client-2', '第二条', 2);
|
||||||
prompt: '第一条',
|
const third = queuedTurn('turn-3', 'client-3', '第三条', 3);
|
||||||
createdAt: 1,
|
|
||||||
});
|
|
||||||
const second = createQueuedChatTurn({
|
|
||||||
id: 'turn-2',
|
|
||||||
prompt: '第二条',
|
|
||||||
createdAt: 2,
|
|
||||||
});
|
|
||||||
const third = createQueuedChatTurn({
|
|
||||||
id: 'turn-3',
|
|
||||||
prompt: '第三条',
|
|
||||||
createdAt: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
let queue = enqueueChatTurn([], first);
|
let queue = enqueueChatTurn([], first);
|
||||||
queue = enqueueChatTurn(queue, second);
|
queue = enqueueChatTurn(queue, second);
|
||||||
@@ -201,15 +209,18 @@ export function registerChatComposerControlTests() {
|
|||||||
|
|
||||||
// FIFO:先入先出,不丢、不乱序。
|
// FIFO:先入先出,不丢、不乱序。
|
||||||
const firstOut = dequeueChatTurn(queue);
|
const firstOut = dequeueChatTurn(queue);
|
||||||
expect(firstOut.next?.prompt).toBe('第一条');
|
expect(firstOut.next?.clientTurnId).toBe('client-1');
|
||||||
expect(firstOut.rest.map((turn) => turn.prompt)).toEqual([
|
expect(firstOut.next && queuedChatTurnLabel(firstOut.next)).toBe('第一条');
|
||||||
|
expect(firstOut.rest.map((turn) => queuedChatTurnLabel(turn))).toEqual([
|
||||||
'第二条',
|
'第二条',
|
||||||
'第三条',
|
'第三条',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 单条取消只移除那一条,顺序不变。
|
// 单条取消只移除那一条,顺序不变。
|
||||||
expect(
|
expect(
|
||||||
removeQueuedChatTurn(firstOut.rest, 'turn-2').map((turn) => turn.prompt),
|
removeQueuedChatTurn(firstOut.rest, 'turn-2').map((turn) =>
|
||||||
|
queuedChatTurnLabel(turn),
|
||||||
|
),
|
||||||
).toEqual(['第三条']);
|
).toEqual(['第三条']);
|
||||||
expect(removeQueuedChatTurn(firstOut.rest, 'turn-missing')).toHaveLength(2);
|
expect(removeQueuedChatTurn(firstOut.rest, 'turn-missing')).toHaveLength(2);
|
||||||
|
|
||||||
@@ -222,11 +233,7 @@ export function registerChatComposerControlTests() {
|
|||||||
for (let index = 0; index < 5; index += 1) {
|
for (let index = 0; index < 5; index += 1) {
|
||||||
queue = enqueueChatTurn(
|
queue = enqueueChatTurn(
|
||||||
queue,
|
queue,
|
||||||
createQueuedChatTurn({
|
queuedTurn(`turn-${index}`, `client-${index}`, `第 ${index} 条`, index),
|
||||||
id: `turn-${index}`,
|
|
||||||
prompt: `第 ${index} 条`,
|
|
||||||
createdAt: index,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expect(isChatTurnQueueFull(queue)).toBe(true);
|
expect(isChatTurnQueueFull(queue)).toBe(true);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user