// Промпт для ИИ-агента (0.87): владелец копирует текст задачи с её страницы и
// отдаёт своему агенту. Внутри — только факты о задаче и ссылка на неё: указаний
// «что делать» здесь нет, это дело владельца, а не кнопки. Собирается из того же
// источника, что и карточки страницы, поэтому расхождений с интерфейсом не будет:
// подписи полей — из локали, значения — теми же форматтерами, что в taskui.
import type { Task } from './api'
import {
deadlineLabel,
formatMinutes,
formatMoney,
priorityLabel,
recurrenceLabel,
statusLabel,
} from './taskui'
/** Функция перевода vue-i18n (нужна только именованная подстановка). */
type Translate = (key: string, named?: Record<string, unknown>) => string
export interface PromptContext {
/** Подзадачи задачи — в промпт идут заголовками и статусами */
subtasks?: Task[]
/** Родительская задача: агент должен видеть, частью чего является задача */
parent?: Task | null
/** Готовая строка аренды («Взято Нави · до 14:30»), если задачу уже держит агент */
lease?: string
/** Ссылка на страницу задачи в интерфейсе */
url: string
}
export function buildTaskPrompt(
task: Task,
context: PromptContext,
t: Translate,
): string {
const lines: string[] = []
const add = (line = '') => lines.push(line)
const field = (label: string, value: unknown) => {
if (value === undefined || value === null || value === '') return
add(`- ${label}: ${value}`)
}
add(t('task.ai.intro'))
add()
add(`## ${t('task.ai.taskSection')}`)
field(t('task.ai.id'), `#${task.id}`)
field(t('task.ai.titleField'), task.title)
field(t('task.ai.status'), statusLabel(task.status))
field(
t('task.field.taskType'),
task.task_type === 'recurring'
? recurrenceLabel(task) || t('stack.recur.recurring')
: t('stack.recur.oneTime'),
)
field(t('task.field.priority'), task.priority === null ? null : priorityLabel(task.priority))
field(
t('task.field.estimate'),
task.estimated_minutes ? formatMinutes(task.estimated_minutes) : null,
)
field(t('task.field.actual'), task.actual_minutes ? formatMinutes(task.actual_minutes) : null)
field(t('task.field.money'), moneyLine(task, t))
field(t('task.field.deadline'), deadlineLabel(task))
field(t('task.field.project'), task.project ? task.project.name : null)
field(t('task.ai.tags'), task.tags.map((tag) => tag.name).join(', '))
field(
t('task.ai.parentTask'),
context.parent ? `#${context.parent.id} ${context.parent.title}` : null,
)
field(t('task.ai.eligible'), task.ai_eligible ? t('task.ai.eligibleYes') : t('task.ai.eligibleNo'))
field(t('task.ai.claimed'), context.lease)
add()
add(`## ${t('task.ai.descriptionSection')}`)
add(task.description.trim() || t('task.ai.descriptionEmpty'))
add()
// Черновик автодетализации: задача ещё не утверждена, но теги, приоритет и
// оценка времени в предложении уже есть — агенту это полезнее пустых полей выше
const proposal = task.ai_proposal
if (proposal) {
const rows: string[] = []
if (proposal.tags.length) rows.push(`- ${t('stack.form.tags')}: ${proposal.tags.join(', ')}`)
if (proposal.priority != null)
rows.push(`- ${t('task.field.priority')}: ${priorityLabel(proposal.priority)}`)
if (proposal.estimated_minutes != null)
rows.push(`- ${t('task.field.estimate')}: ${formatMinutes(proposal.estimated_minutes)}`)
if (rows.length) {
add(`## ${t('task.ai.proposalSection')}`)
rows.forEach(add)
add()
}
}
const subtasks = context.subtasks ?? []
if (subtasks.length) {
add(`## ${t('task.ai.subtasksSection')}`)
subtasks.forEach((child) => add(`- #${child.id} ${child.title} — ${statusLabel(child.status)}`))
add()
}
add(`## ${t('task.ai.linkSection')}`)
add(context.url)
return lines.join('\n')
}
/** «бюджет 5 000 ₴ · оценка 3 000 ₴» — как в сайдбаре параметров страницы. */
function moneyLine(task: Task, t: Translate): string {
return [
task.budget_money !== null
? t('taskui.money.budget', { amount: formatMoney(task.budget_money) })
: '',
task.cost_estimate_money !== null
? t('taskui.money.estimate', { amount: formatMoney(task.cost_estimate_money) })
: '',
]
.filter(Boolean)
.join(' · ')
}