diff --git a/css/index.css b/css/index.css index 14514da..f3da6e3 100644 --- a/css/index.css +++ b/css/index.css @@ -337,3 +337,11 @@ .ClaudeCodePreview--error .ClaudeCodePreview-content { color: #c62828; } + +.ClaudeCodePreview-tags { + margin-top: 8px; + padding-top: 8px; + border-top: 1px dashed #ccc; + font-size: 12px; + color: #666; +} diff --git a/src/browser/Action/ServiceAction.js b/src/browser/Action/ServiceAction.js index 50d4cfd..fd97e31 100644 --- a/src/browser/Action/ServiceAction.js +++ b/src/browser/Action/ServiceAction.js @@ -195,7 +195,7 @@ export default class ServiceAction extends Action { } // Claude Code関連アクション - runClaudeCode(url, title, config) { + runClaudeCode(url, title, config, relatedItems = [], availableTags = []) { if (!config?.enabled) return; if (!fs.existsSync(config.cliPath)) return; if (!fs.existsSync(config.workDir)) { @@ -207,7 +207,7 @@ export default class ServiceAction extends Action { const prompt = typeof config.prompt === "function" - ? config.prompt({ url, title }) + ? config.prompt({ url, title, relatedItems, availableTags }) : `${config.prompt}\n\nURL: ${url}\nTitle: ${title}`; const args = []; @@ -234,9 +234,27 @@ export default class ServiceAction extends Action { claudeProcess.on("close", (code) => { if (code === 0 && stdout) { - const match = stdout.match(/```(?:markdown)?\s*([\s\S]*?)```/); - const result = match ? match[1].trim() : stdout.trim(); - this.dispatch(keys.claudeCodeComplete, { url, result }); + // JSONコードブロックを抽出 + const jsonMatch = stdout.match(/```(?:json)?\s*([\s\S]*?)```/); + let parsedResult = { comment: "", tags: [] }; + + if (jsonMatch) { + try { + parsedResult = JSON.parse(jsonMatch[1].trim()); + } catch (e) { + // フォールバック: 従来のmarkdown形式 + const markdownMatch = stdout.match(/```(?:markdown)?\s*([\s\S]*?)```/); + parsedResult.comment = markdownMatch ? markdownMatch[1].trim() : stdout.trim(); + } + } else { + parsedResult.comment = stdout.trim(); + } + + this.dispatch(keys.claudeCodeComplete, { + url, + comment: parsedResult.comment, + tags: Array.isArray(parsedResult.tags) ? parsedResult.tags : [] + }); } else { this.dispatch(keys.claudeCodeError, { url, error: stderr || `Exit code: ${code}` }); } diff --git a/src/browser/App.js b/src/browser/App.js index e5568a8..7f8aac9 100644 --- a/src/browser/App.js +++ b/src/browser/App.js @@ -187,6 +187,25 @@ class App extends React.Component { }; const finishEditing = (relatedItem, value) => { ServiceAction.finishEditingRelatedItem(relatedItem, value); + + // 関連URL追加時に再推論 + if ( + this._claudeCodeConfig?.enabled && + this.state.claudeCode.status !== "loading" && + this.state.URL?.startsWith("http") + ) { + // 少し遅延を入れてstateが更新されてから実行 + setTimeout(() => { + const updatedRelatedItems = appContext.ServiceStore.state.relatedItems; + ServiceAction.runClaudeCode( + this.state.URL, + this.state.title, + this._claudeCodeConfig, + updatedRelatedItems, + this.state.tags + ); + }, 100); + } }; const addItem = () => { ServiceAction.addRelatedItem(); @@ -194,8 +213,8 @@ class App extends React.Component { const submitPostLink = this.postLink.bind(this); // Claude Code関連 - const runClaudeCode = (url, title, config) => { - ServiceAction.runClaudeCode(url, title, config); + const runClaudeCode = (url, title, config, relatedItems = [], availableTags = []) => { + ServiceAction.runClaudeCode(url, title, config, relatedItems, availableTags); }; const insertClaudeCodeResult = () => { ServiceAction.insertClaudeCodeResult(); @@ -224,6 +243,8 @@ class App extends React.Component { insertResult={insertClaudeCodeResult} clearResult={clearClaudeCodeResult} claudeCodeConfig={this._claudeCodeConfig} + relatedItems={this.state.relatedItems} + availableTags={this.state.tags} /> diff --git a/src/browser/Store/ServiceStore.js b/src/browser/Store/ServiceStore.js index 9920950..d2b0baa 100644 --- a/src/browser/Store/ServiceStore.js +++ b/src/browser/Store/ServiceStore.js @@ -23,7 +23,8 @@ export default class ServiceStore extends Store { claudeCode: { status: "idle", // idle | loading | complete | error url: null, - result: null, + comment: null, + tags: [], error: null } }; @@ -86,7 +87,8 @@ export default class ServiceStore extends Store { claudeCode: { status: "idle", url: null, - result: null, + comment: null, + tags: [], error: null } }); @@ -137,18 +139,20 @@ export default class ServiceStore extends Store { claudeCode: { status: "loading", url, - result: null, + comment: null, + tags: [], error: null } }); }); - this.register(keys.claudeCodeComplete, ({ url, result }) => { + this.register(keys.claudeCodeComplete, ({ url, comment, tags }) => { this.setState({ claudeCode: { status: "complete", url, - result, + comment, + tags: tags || [], error: null } }); @@ -159,7 +163,8 @@ export default class ServiceStore extends Store { claudeCode: { status: "error", url, - result: null, + comment: null, + tags: [], error } }); @@ -170,22 +175,29 @@ export default class ServiceStore extends Store { claudeCode: { status: "idle", url: null, - result: null, + comment: null, + tags: [], error: null } }); }); this.register(keys.claudeCodeInsert, () => { - const { claudeCode } = this.state; - if (claudeCode.result) { - // 結果でコメントを入れ替え + const { claudeCode, selectedTags } = this.state; + if (claudeCode.comment) { + // タグをマージ(重複排除) + const newTags = claudeCode.tags || []; + const mergedTags = [...new Set([...selectedTags, ...newTags])]; + + // 結果でコメントを入れ替え、タグも追加 this.setState({ - comment: claudeCode.result, + comment: claudeCode.comment, + selectedTags: mergedTags, claudeCode: { status: "idle", url: null, - result: null, + comment: null, + tags: [], error: null } }); diff --git a/src/browser/component/ClaudeCodeButton.js b/src/browser/component/ClaudeCodeButton.js index 73ba7e9..41a09b7 100644 --- a/src/browser/component/ClaudeCodeButton.js +++ b/src/browser/component/ClaudeCodeButton.js @@ -9,7 +9,9 @@ export default function ClaudeCodeButton({ runClaudeCode, insertResult, clearResult, - claudeCodeConfig + claudeCodeConfig, + relatedItems = [], + availableTags = [] }) { const prevUrlRef = useRef(url); const debounceTimerRef = useRef(null); @@ -29,7 +31,7 @@ export default function ClaudeCodeButton({ // 1秒後に実行(入力中の連続変更を避ける) debounceTimerRef.current = setTimeout(() => { - runClaudeCode(url, title, claudeCodeConfig); + runClaudeCode(url, title, claudeCodeConfig, relatedItems, availableTags); }, 1000); } @@ -40,7 +42,7 @@ export default function ClaudeCodeButton({ clearTimeout(debounceTimerRef.current); } }; - }, [url, title, claudeCodeConfig, runClaudeCode]); + }, [url, title, claudeCodeConfig, runClaudeCode, relatedItems, availableTags]); const handleClick = useCallback(() => { if (claudeCode.status === "complete") { @@ -49,10 +51,10 @@ export default function ClaudeCodeButton({ } else if (claudeCode.status === "idle" || claudeCode.status === "error") { // アイドルまたはエラー状態の場合は実行 if (url && url.startsWith("http")) { - runClaudeCode(url, title, claudeCodeConfig); + runClaudeCode(url, title, claudeCodeConfig, relatedItems, availableTags); } } - }, [claudeCode.status, url, title, claudeCodeConfig, runClaudeCode, insertResult]); + }, [claudeCode.status, url, title, claudeCodeConfig, runClaudeCode, insertResult, relatedItems, availableTags]); // 設定が無効またはCLIが設定されていない場合は表示しない if (!claudeCodeConfig?.enabled) { @@ -70,7 +72,7 @@ export default function ClaudeCodeButton({ ); case "complete": return ( - + AI ✓ ); diff --git a/src/browser/component/ClaudeCodePreview.js b/src/browser/component/ClaudeCodePreview.js index 75eb1f3..548567c 100644 --- a/src/browser/component/ClaudeCodePreview.js +++ b/src/browser/component/ClaudeCodePreview.js @@ -21,7 +21,7 @@ export default function ClaudeCodePreview({ claudeCode, insertResult, clearResul ); } - if (claudeCode.status === "complete" && claudeCode.result) { + if (claudeCode.status === "complete" && claudeCode.comment) { return ( @@ -32,8 +32,11 @@ export default function ClaudeCodePreview({ claudeCode, insertResult, clearResul - {claudeCode.result} + {claudeCode.comment} + {claudeCode.tags && claudeCode.tags.length > 0 && ( + Tags: {claudeCode.tags.join(", ")} + )} ); }