プロジェクト

全般

プロフィール

機能 #194

未完了

チケット詳細画面 - AI支援による子課題自動作成機能

Redmine Admin さんが4日前に追加.

ステータス:
新規
優先度:
高め
担当者:
開始日:
2025-06-04
期日:
進捗率:

0%

予定工数:

説明

機能要望概要

対象画面: チケット詳細画面(例: https://task.call2arm.com/redmine-ui/tickets/192)
設置箇所: 詳細情報欄の下部
要望内容: AI支援による子課題自動作成機能

機能仕様

1. ボタン設置場所

CSSセレクタ: #root > div > div.min-h-screen.bg-gray-50 > div.flex > main > div > div > div.bg-white.rounded-lg.shadow-sm > div:nth-child(2) > div > div.md\:col-span-1 > div:nth-child(1)

HTML構造:

<div className="ticket-details-section">
  {/* 既存の詳細情報 */}
  <div className="detail-info">
    <div className="project-info">プロジェクト: Redmineの設定</div>
    <div className="tracker-info">トラッカー: 機能</div>
    <div className="assignee-info">担当者: Redmine Admin</div>
    <div className="progress-info">進捗: 0%</div>
  </div>
  
  {/* 新規追加: 子課題作成ボタン */}
  <div className="child-ticket-actions mt-4 pt-4 border-t">
    <button 
      className="btn btn-primary flex items-center gap-2"
      onClick={handleCreateChildTickets}
    >
      🎯 子課題を作成
    </button>
  </div>
</div>

2. AI対話による課題分解プロセス

A. 初期質問フォーム

<div className="child-ticket-wizard">
  <div className="wizard-header">
    <h3 className="text-lg font-bold">AI支援による子課題作成</h3>
    <p className="text-sm text-gray-600">親課題の内容を分析して、効果的な子課題を提案します</p>
  </div>
  
  <div className="parent-ticket-context bg-gray-50 p-4 rounded-lg mb-6">
    <h4 className="font-semibold">親課題情報</h4>
    <div className="text-sm">
      <p><strong>件名:</strong> {parentTicket.subject}</p>
      <p><strong>説明:</strong> {truncateText(parentTicket.description, 200)}</p>
      <p><strong>プロジェクト:</strong> {parentTicket.project.name}</p>
    </div>
  </div>
  
  <div className="wizard-questions space-y-4">
    <div className="question-group">
      <label className="block font-medium mb-2">🎯 目的は何ですか?</label>
      <textarea 
        className="w-full border rounded-lg p-3"
        placeholder="この課題を解決することで何を達成したいですか?"
        value={formData.purpose}
        onChange={(e) => setFormData({...formData, purpose: e.target.value})}
      />
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">📋 誰が報告を受ける人ですか?</label>
      <select 
        className="w-full border rounded-lg p-3"
        value={formData.reporter}
        onChange={(e) => setFormData({...formData, reporter: e.target.value})}
      >
        <option value="">選択してください</option>
        {projectMembers.map(member => (
          <option key={member.id} value={member.id}>{member.name}</option>
        ))}
      </select>
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">👥 誰が作業をする人ですか?</label>
      <div className="space-y-2">
        <select 
          className="w-full border rounded-lg p-3"
          multiple
          value={formData.assignees}
          onChange={handleAssigneeChange}
        >
          {projectMembers.map(member => (
            <option key={member.id} value={member.id}>{member.name}</option>
          ))}
        </select>
        <textarea 
          className="w-full border rounded-lg p-3"
          placeholder="複数人いる場合は作業分担を教えてください"
          value={formData.workDivision}
          onChange={(e) => setFormData({...formData, workDivision: e.target.value})}
        />
      </div>
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">🎯 どんな結果をゴールに設定しますか?</label>
      <textarea 
        className="w-full border rounded-lg p-3"
        placeholder="期待する成果・完了条件を具体的に記述してください"
        value={formData.goal}
        onChange={(e) => setFormData({...formData, goal: e.target.value})}
      />
      <div className="mt-2">
        <label className="block text-sm font-medium mb-1">📅 納期はいつですか?</label>
        <input 
          type="date"
          className="border rounded-lg p-2"
          value={formData.dueDate}
          onChange={(e) => setFormData({...formData, dueDate: e.target.value})}
        />
      </div>
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">🛣️ マイルストーンとして何が必要ですか?</label>
      <textarea 
        className="w-full border rounded-lg p-3"
        placeholder="ゴール達成のための主要な節目・チェックポイントを教えてください"
        value={formData.milestones}
        onChange={(e) => setFormData({...formData, milestones: e.target.value})}
      />
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">📄 中間成果物、成果物は何が必要ですか?</label>
      <textarea 
        className="w-full border rounded-lg p-3"
        placeholder="ドキュメント、設計書、コード、テスト結果など具体的な成果物を記述してください"
        value={formData.deliverables}
        onChange={(e) => setFormData({...formData, deliverables: e.target.value})}
      />
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">⚙️ そのためにどんな作業が必要ですか?</label>
      <textarea 
        className="w-full border rounded-lg p-3"
        placeholder="必要な作業・タスクを思いつく限り列挙してください"
        value={formData.requiredWork}
        onChange={(e) => setFormData({...formData, requiredWork: e.target.value})}
      />
    </div>
    
    <div className="question-group">
      <label className="block font-medium mb-2">🎫 何個くらいのチケットに分けますか?</label>
      <div className="flex items-center gap-4">
        <input 
          type="number"
          className="border rounded-lg p-2 w-20"
          min="1"
          max="20"
          value={formData.ticketCount}
          onChange={(e) => setFormData({...formData, ticketCount: e.target.value})}
        />
        <span className="text-sm text-gray-600"></span>
        <label className="flex items-center">
          <input 
            type="checkbox"
            checked={formData.autoCount}
            onChange={(e) => setFormData({...formData, autoCount: e.target.checked})}
          />
          <span className="ml-2 text-sm">おまかせ(AIが最適な数を提案)</span>
        </label>
      </div>
    </div>
  </div>
  
  <div className="wizard-actions mt-6 flex gap-3">
    <button 
      className="btn btn-primary flex items-center gap-2"
      onClick={generateChildTicketProposal}
      disabled={!isFormValid()}
    >
      🤖 AIに子課題を提案してもらう
    </button>
    <button 
      className="btn btn-secondary"
      onClick={closeWizard}
    >
      キャンセル
    </button>
  </div>
</div>

B. AI提案結果表示

<div className="ai-proposal-results">
  <div className="proposal-header">
    <h3 className="text-lg font-bold">🤖 AIによる子課題提案</h3>
    <p className="text-sm text-gray-600">以下の子課題構成を提案します。必要に応じて編集してから作成してください。</p>
  </div>
  
  {/* 提案概要 */}
  <div className="proposal-summary bg-blue-50 p-4 rounded-lg mb-6">
    <h4 className="font-semibold mb-2">📋 提案概要</h4>
    <div className="text-sm space-y-1">
      <p><strong>総チケット数:</strong> {proposalData.totalTickets}</p>
      <p><strong>推定工期:</strong> {proposalData.estimatedDuration}</p>
      <p><strong>主要マイルストーン:</strong> {proposalData.majorMilestones}</p>
    </div>
  </div>
  
  {/* 子課題リスト */}
  <div className="proposed-tickets space-y-4">
    {proposalData.childTickets.map((ticket, index) => (
      <div key={index} className="ticket-proposal border rounded-lg p-4">
        <div className="ticket-header flex justify-between items-start mb-3">
          <div className="flex-1">
            <input 
              type="text"
              className="w-full font-semibold text-lg border-b border-gray-300 focus:border-blue-500 outline-none"
              value={ticket.subject}
              onChange={(e) => updateTicketProposal(index, 'subject', e.target.value)}
            />
          </div>
          <div className="flex items-center gap-2 ml-4">
            <span className="text-xs bg-gray-100 px-2 py-1 rounded">#{index + 1}</span>
            <button 
              className="text-red-500 hover:text-red-700"
              onClick={() => removeTicketProposal(index)}
            >
              🗑️
            </button>
          </div>
        </div>
        
        <div className="ticket-details grid grid-cols-1 md:grid-cols-2 gap-4">
          <div>
            <label className="block text-sm font-medium mb-1">説明</label>
            <textarea 
              className="w-full border rounded p-2 text-sm"
              rows="3"
              value={ticket.description}
              onChange={(e) => updateTicketProposal(index, 'description', e.target.value)}
            />
          </div>
          
          <div className="space-y-3">
            <div>
              <label className="block text-sm font-medium mb-1">担当者</label>
              <select 
                className="w-full border rounded p-2"
                value={ticket.assignee}
                onChange={(e) => updateTicketProposal(index, 'assignee', e.target.value)}
              >
                <option value="">未割当て</option>
                {projectMembers.map(member => (
                  <option key={member.id} value={member.id}>{member.name}</option>
                ))}
              </select>
            </div>
            
            <div className="grid grid-cols-2 gap-2">
              <div>
                <label className="block text-sm font-medium mb-1">優先度</label>
                <select 
                  className="w-full border rounded p-2"
                  value={ticket.priority}
                  onChange={(e) => updateTicketProposal(index, 'priority', e.target.value)}
                >
                  <option value="2">通常</option>
                  <option value="3">高め</option>
                  <option value="4">緊急</option>
                  <option value="1">低め</option>
                </select>
              </div>
              
              <div>
                <label className="block text-sm font-medium mb-1">期限</label>
                <input 
                  type="date"
                  className="w-full border rounded p-2"
                  value={ticket.dueDate}
                  onChange={(e) => updateTicketProposal(index, 'dueDate', e.target.value)}
                />
              </div>
            </div>
            
            <div>
              <label className="block text-sm font-medium mb-1">推定工数</label>
              <div className="flex items-center gap-2">
                <input 
                  type="number"
                  className="border rounded p-2 w-20"
                  step="0.5"
                  value={ticket.estimatedHours}
                  onChange={(e) => updateTicketProposal(index, 'estimatedHours', e.target.value)}
                />
                <span className="text-sm text-gray-600">時間</span>
              </div>
            </div>
          </div>
        </div>
        
        {/* 依存関係表示 */}
        {ticket.dependencies && ticket.dependencies.length > 0 && (
          <div className="dependencies mt-3 p-2 bg-yellow-50 rounded">
            <span className="text-sm font-medium">📎 依存関係:</span>
            <span className="text-sm ml-2">
              {ticket.dependencies.map(dep => `#${dep}`).join(', ')} の完了後に着手
            </span>
          </div>
        )}
      </div>
    ))}
  </div>
  
  {/* アクション */}
  <div className="proposal-actions mt-6 flex gap-3">
    <button 
      className="btn btn-success flex items-center gap-2"
      onClick={createChildTickets}
    >
      ✅ この提案で子課題を作成
    </button>
    <button 
      className="btn btn-secondary flex items-center gap-2"
      onClick={addTicketProposal}
    >
      ➕ チケットを追加
    </button>
    <button 
      className="btn btn-primary flex items-center gap-2"
      onClick={regenerateProposal}
    >
      🔄 再提案
    </button>
    <button 
      className="btn btn-secondary"
      onClick={backToQuestions}
    >
      ← 質問に戻る
    </button>
  </div>
</div>

3. AI提案ロジック

A. プロンプト設計

const generateChildTicketsPrompt = (parentTicket, formData) => {
  return `
あなたはプロジェクト管理の専門家です。以下の親課題を効果的な子課題に分解してください。

【親課題情報】
件名: ${parentTicket.subject}
説明: ${parentTicket.description}
プロジェクト: ${parentTicket.project.name}

【要件】
目的: ${formData.purpose}
担当者: ${formData.assignees.map(id => getUserName(id)).join(', ')}
作業分担: ${formData.workDivision}
ゴール: ${formData.goal}
納期: ${formData.dueDate}
マイルストーン: ${formData.milestones}
成果物: ${formData.deliverables}
必要作業: ${formData.requiredWork}
希望チケット数: ${formData.autoCount ? 'おまかせ' : formData.ticketCount + ''}

【提案要求】
1. 効果的な子課題の分解(件名・説明・担当者・期限・工数見積もり)
2. 作業の依存関係の明確化
3. リスク軽減のための並行作業の提案
4. 各子課題の成果物と完了条件

JSON形式で回答してください:
{
  "totalTickets": number,
  "estimatedDuration": "string",
  "majorMilestones": "string",
  "childTickets": [
    {
      "subject": "string",
      "description": "string", 
      "assignee": number,
      "priority": number,
      "dueDate": "YYYY-MM-DD",
      "estimatedHours": number,
      "dependencies": [number] // 他のチケットのインデックス
    }
  ]
}
`;
};

B. API実装

// 子課題提案API
app.post('/api/tickets/:id/generate-children', async (req, res) => {
  const { parentTicketId } = req.params;
  const formData = req.body;
  
  try {
    // 親課題情報取得
    const parentTicket = await getTicket(parentTicketId);
    
    // Claude APIで子課題提案生成
    const prompt = generateChildTicketsPrompt(parentTicket, formData);
    const aiResponse = await claudeAPI.generateResponse(prompt);
    
    // 提案データの解析・検証
    const proposalData = JSON.parse(aiResponse);
    
    res.json({
      success: true,
      proposal: proposalData,
      parentTicket: parentTicket
    });
    
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// 子課題一括作成API
app.post('/api/tickets/:id/create-children', async (req, res) => {
  const { parentTicketId } = req.params;
  const { childTickets } = req.body;
  
  try {
    const createdTickets = [];
    
    for (const childTicket of childTickets) {
      const newTicket = await createTicket({
        ...childTicket,
        parent_issue_id: parentTicketId,
        project_id: parentTicket.project.id
      });
      createdTickets.push(newTicket);
    }
    
    res.json({
      success: true,
      createdTickets: createdTickets
    });
    
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

4. 期待効果

  • プロジェクト分解の効率化: 大きな課題の構造化
  • 作業計画の最適化: AI支援による現実的なスケジュール提案
  • 担当者配分の改善: スキルと工数を考慮した人員配置
  • 進捗管理の向上: 明確なマイルストーンと依存関係
  • 品質向上: 成果物と完了条件の明確化

5. 関連チケット

  • #192: AIアシスタント機能拡張(AI機能の基盤)
  • #191: Claude API接続問題(前提条件として解決必要)

6. 優先度

  • 緊急度: 中(プロジェクト管理効率化)
  • 重要度: 高(AI支援による業務改善の中核機能)

表示するデータがありません

他の形式にエクスポート: Atom PDF