Claude 3.7 Sonnetのコーディング能力を試す 5
以前の投稿でClaude 3.7 Sonnetのコーディング能力を試した.その際に描画能力が独特なことをまとめた.そこでSyntax Treeを描画するコードが書けないか試してみた.プロンプトと結果は下記.崩れはあるが一発でこれが出るのはすごい.
言語学のSyntax Tree をリッチに描画できるプログラムを書いて.
React上で動くように使い勝手の良いものを
import React, { useState, useEffect } from 'react';
// SVGで統語木を描画するメインコンポーネント
export default function SyntaxTreeBuilder() {
const [treeInput, setTreeInput] = useState('(S (NP (DET The) (N cat)) (VP (V chased) (NP (DET the) (N dog))))');
const [fontSize, setFontSize] = useState(14);
const [nodeDistance, setNodeDistance] = useState(40);
const [levelHeight, setLevelHeight] = useState(60);
const [colorScheme, setColorScheme] = useState('default');
const [showHelp, setShowHelp] = useState(false);
const [error, setError] = useState(null);
const [tree, setTree] = useState(null);
const [svgDimensions, setSvgDimensions] = useState({ width: 800, height: 400 });
// カラースキーム定義
const colorSchemes = {
default: {
S: '#4299e1', // 青
NP: '#68d391', // 緑
VP: '#f6ad55', // オレンジ
PP: '#b794f4', // 紫
CP: '#f687b3', // ピンク
DP: '#9ae6b4', // 淡い緑
IP: '#90cdf4', // 淡い青
AP: '#fbd38d', // 淡いオレンジ
default: '#a0aec0', // グレー
},
pastel: {
S: '#bee3f8', // 淡い青
NP: '#c6f6d5', // 淡い緑
VP: '#feebc8', // 淡いオレンジ
PP: '#e9d8fd', // 淡い紫
CP: '#fed7e2', // 淡いピンク
DP: '#c6f6d5', // 淡い緑
IP: '#bee3f8', // 淡い青
AP: '#feebc8', // 淡いオレンジ
default: '#e2e8f0', // 淡いグレー
},
monochrome: {
S: '#2d3748',
NP: '#4a5568',
VP: '#718096',
PP: '#a0aec0',
CP: '#cbd5e0',
DP: '#e2e8f0',
IP: '#edf2f7',
AP: '#f7fafc',
default: '#a0aec0',
}
};
// ブラケット表記を解析して木構造に変換
const parseTree = (input) => {
try {
// 入力文字列が空の場合
if (!input.trim()) {
return null;
}
// カッコの数が一致するか確認
const openBrackets = (input.match(/\(/g) || []).length;
const closeBrackets = (input.match(/\)/g) || []).length;
if (openBrackets !== closeBrackets) {
throw new Error('括弧の数が一致しません');
}
// 正規表現で不正な構文をチェック
if (!/^\s*\([A-Za-z0-9]+ /.test(input)) {
throw new Error('構文が正しくありません');
}
let index = 0;
const parseNode = () => {
// 空白をスキップ
while (index < input.length && /\s/.test(input[index])) {
index++;
}
if (index >= input.length) {
return null;
}
// 開始括弧をチェック
if (input[index] === '(') {
index++; // 開始括弧をスキップ
// ラベルを取得
let label = '';
while (index < input.length && /[A-Za-z0-9]/.test(input[index])) {
label += input[index++];
}
// 少なくとも1文字のスペースが必要
if (index < input.length && input[index] !== ' ') {
throw new Error('ラベルの後にスペースが必要です');
}
// 空白をスキップ
while (index < input.length && /\s/.test(input[index])) {
index++;
}
const children = [];
// 子ノードを取得
while (index < input.length && input[index] !== ')') {
if (input[index] === '(') {
// 子ノードが括弧で始まる場合は再帰的に解析
const childNode = parseNode();
if (childNode) {
children.push(childNode);
}
} else {
// 終端記号(単語)を取得
let word = '';
while (index < input.length && !/[\s\(\)]/.test(input[index])) {
word += input[index++];
}
if (word) {
children.push({ label: word, isLeaf: true });
}
}
// 空白をスキップ
while (index < input.length && /\s/.test(input[index])) {
index++;
}
}
if (index >= input.length) {
throw new Error('閉じ括弧がありません');
}
index++; // 閉じ括弧をスキップ
return { label, children };
}
return null;
};
const result = parseNode();
// 残りの文字列をチェック(余分な文字がある場合はエラー)
while (index < input.length) {
if (!/\s/.test(input[index])) {
throw new Error('入力の末尾に余分な文字があります');
}
index++;
}
return result;
} catch (err) {
setError(err.message);
return null;
}
};
// 木構造を計算してSVG描画用のデータを生成
const calculateTreeLayout = (root) => {
if (!root) return null;
// 木の深さと幅を計算
const getTreeDimensions = (node, depth = 0) => {
if (!node) return { depth: 0, width: 0 };
if (node.isLeaf) {
return { depth, width: 1 };
}
let maxChildDepth = depth;
let totalWidth = 0;
for (const child of node.children) {
const childDim = getTreeDimensions(child, depth + 1);
maxChildDepth = Math.max(maxChildDepth, childDim.depth);
totalWidth += childDim.width;
}
return { depth: maxChildDepth, width: Math.max(1, totalWidth) };
};
const dimensions = getTreeDimensions(root);
const treeDepth = dimensions.depth;
const treeWidth = dimensions.width;
// SVGのサイズを調整
const width = Math.max(800, treeWidth * nodeDistance * 3);
const height = (treeDepth + 1) * levelHeight + 80; // 余白を追加
setSvgDimensions({ width, height });
// ノードの位置を計算
const calculateNodePositions = (node, depth = 0, offset = 0) => {
if (!node) return { node: null, width: 0 };
const nodeWidth = node.isLeaf ? 1 : node.children.reduce((sum, child) => {
const childCalc = calculateNodePositions(child, depth + 1, offset + sum);
child.x = childCalc.x;
child.y = (depth + 1) * levelHeight + 40; // 上部に余白を追加
return sum + childCalc.width;
}, 0);
// このノード自体の位置を計算
node.x = offset + nodeWidth / 2;
node.y = depth * levelHeight + 40; // 上部に余白を追加
return { node, width: nodeWidth, x: node.x };
};
calculateNodePositions(root);
return root;
};
// 木構造を再計算
useEffect(() => {
try {
setError(null);
const parsedTree = parseTree(treeInput);
const calculatedTree = calculateTreeLayout(parsedTree);
setTree(calculatedTree);
} catch (err) {
setError(err.message || '構文解析エラー');
}
}, [treeInput, nodeDistance, levelHeight]);
// SVGで線を描画
const renderLines = (node) => {
if (!node || node.isLeaf || !node.children || node.children.length === 0) {
return null;
}
return node.children.map((child, index) => (
<React.Fragment key={`line-${node.x}-${node.y}-${index}`}>
<line
x1={node.x}
y1={node.y + 10}
x2={child.x}
y2={child.y - 10}
stroke="black"
strokeWidth="1.5"
/>
{renderLines(child)}
</React.Fragment>
));
};
// SVGでノードを描画
const renderNodes = (node) => {
if (!node) return null;
const getNodeColor = (label) => {
// ラベルの最初の部分(例: NPからNを抽出)を使用
const categoryLabel = label.split('-')[0];
return colorSchemes[colorScheme][categoryLabel] || colorSchemes[colorScheme].default;
};
return (
<React.Fragment>
<g transform={`translate(${node.x}, ${node.y})`}>
{!node.isLeaf ? (
<rect
x={-(node.label.length * fontSize * 0.4) / 2}
y={-15}
width={node.label.length * fontSize * 0.4 + 20}
height={30}
rx={5}
ry={5}
fill={getNodeColor(node.label)}
fillOpacity={0.7}
stroke="black"
strokeWidth="1"
/>
) : null}
<text
textAnchor="middle"
dominantBaseline="middle"
fontSize={fontSize}
fontFamily="Arial"
fontWeight={node.isLeaf ? "normal" : "bold"}
>
{node.label}
</text>
</g>
{node.children && node.children.map((child, index) => renderNodes(child))}
</React.Fragment>
);
};
// サンプル文を設定
const setSampleTree = (sample) => {
setTreeInput(sample);
};
return (
<div className="flex flex-col max-w-6xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">リッチな統語木ビルダー</h1>
<div className="mb-4 flex flex-wrap gap-2">
<button
onClick={() => setSampleTree('(S (NP (DET The) (N cat)) (VP (V chased) (NP (DET the) (N dog))))')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
基本例
</button>
<button
onClick={() => setSampleTree('(CP (C that) (IP (NP (DET the) (N student)) (I\' (I will) (VP (V study) (NP (N linguistics))))))')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
CP例
</button>
<button
onClick={() => setSampleTree('(S (NP (DET その) (N 猫)) (VP (PP (NP (N 魚)) (P を)) (V 食べた)))')}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600"
>
日本語例
</button>
<button
onClick={() => setShowHelp(!showHelp)}
className="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600"
>
{showHelp ? 'ヘルプを隠す' : 'ヘルプを表示'}
</button>
</div>
{showHelp && (
<div className="mb-4 p-4 bg-gray-100 rounded-lg">
<h3 className="font-bold mb-2">使い方:</h3>
<p className="mb-2">括弧表記で統語木を入力してください。括弧表記の例:</p>
<pre className="bg-gray-200 p-2 rounded">(S (NP (DET The) (N cat)) (VP (V chased) (NP (DET the) (N dog))))</pre>
<p className="mt-2">各ノードは (ラベル 内容) の形式で表現されます。内容は他のノードまたは単語です。</p>
</div>
)}
<div className="mb-4">
<label className="block mb-2 font-semibold">統語木の括弧表記:</label>
<textarea
value={treeInput}
onChange={(e) => setTreeInput(e.target.value)}
className="w-full p-2 border rounded h-24"
placeholder="(S (NP (DET The) (N cat)) (VP (V chased) (NP (DET the) (N dog))))"
/>
{error && <p className="text-red-500 mt-1">{error}</p>}
</div>
<div className="mb-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block mb-2 font-semibold">フォントサイズ: {fontSize}</label>
<input
type="range"
min="10"
max="20"
value={fontSize}
onChange={(e) => setFontSize(parseInt(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block mb-2 font-semibold">ノード間距離: {nodeDistance}</label>
<input
type="range"
min="20"
max="80"
value={nodeDistance}
onChange={(e) => setNodeDistance(parseInt(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block mb-2 font-semibold">レベルの高さ: {levelHeight}</label>
<input
type="range"
min="40"
max="100"
value={levelHeight}
onChange={(e) => setLevelHeight(parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
<div className="mb-4">
<label className="block mb-2 font-semibold">カラースキーム:</label>
<div className="flex gap-3">
<button
onClick={() => setColorScheme('default')}
className={`px-3 py-1 rounded ${colorScheme === 'default' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
デフォルト
</button>
<button
onClick={() => setColorScheme('pastel')}
className={`px-3 py-1 rounded ${colorScheme === 'pastel' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
パステル
</button>
<button
onClick={() => setColorScheme('monochrome')}
className={`px-3 py-1 rounded ${colorScheme === 'monochrome' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}
>
モノクローム
</button>
</div>
</div>
<div className="overflow-auto border rounded p-4">
<svg width={svgDimensions.width} height={svgDimensions.height}>
{tree && renderLines(tree)}
{tree && renderNodes(tree)}
</svg>
</div>
</div>
);
}
