media preview fix

This commit is contained in:
Yared Yemane 2026-04-07 10:59:40 -07:00
parent 4210a05ba9
commit d1842579e9
4 changed files with 853 additions and 200 deletions

21
src/lib/practiceMedia.ts Normal file
View File

@ -0,0 +1,21 @@
import { resolveFileUrl } from "../api/files.api"
export function normalizeObjectKey(value: string): string {
const trimmed = value.trim()
if (!trimmed) return ""
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed
const protocolMatch = trimmed.match(/^[a-z]+:\/\//i)
if (protocolMatch) {
return trimmed.replace(/^[a-z]+:\/\//i, "")
}
return trimmed
}
export async function resolveMediaPreviewUrl(value: string): Promise<string> {
if (!value.trim()) return ""
if (value.startsWith("http://") || value.startsWith("https://")) return value
const key = normalizeObjectKey(value)
if (!key) return ""
const res = await resolveFileUrl(key)
return res.data?.data?.url ?? ""
}

View File

@ -40,6 +40,7 @@ interface Question {
sampleAnswerVoicePrompt: string
audioCorrectAnswerText: string
shortAnswers: string[]
imageUrl: string
}
const PERSONAS: Persona[] = [
@ -91,6 +92,7 @@ function createEmptyQuestion(id: string): Question {
sampleAnswerVoicePrompt: "",
audioCorrectAnswerText: "",
shortAnswers: [],
imageUrl: "",
}
}
@ -232,6 +234,7 @@ export function AddNewPracticePage() {
voice_prompt: q.voicePrompt || undefined,
sample_answer_voice_prompt: q.sampleAnswerVoicePrompt || undefined,
audio_correct_answer_text: q.audioCorrectAnswerText || undefined,
image_url: q.imageUrl.trim() || undefined,
short_answers: q.shortAnswers.length > 0 ? q.shortAnswers : undefined,
})
@ -606,6 +609,7 @@ export function AddNewPracticePage() {
sampleAnswerVoicePrompt: question.sampleAnswerVoicePrompt,
audioCorrectAnswerText: question.audioCorrectAnswerText,
shortAnswer: question.shortAnswers[0] ?? "",
imageUrl: question.imageUrl,
}}
onChange={(next) => {
updateQuestion(question.id, {
@ -620,8 +624,10 @@ export function AddNewPracticePage() {
sampleAnswerVoicePrompt: next.sampleAnswerVoicePrompt,
audioCorrectAnswerText: next.audioCorrectAnswerText,
shortAnswers: next.shortAnswer.trim() ? [next.shortAnswer.trim()] : [],
imageUrl: next.imageUrl,
})
}}
mediaBusy={saving}
/>
</Card>
))}

View File

@ -210,7 +210,6 @@ export function HumanLanguagePage() {
const [questionDialog, setQuestionDialog] = useState<QuestionDialogState>({ open: false })
const [practiceForm, setPracticeForm] = useState({ title: "", description: "", persona: "" })
const [questionDraft, setQuestionDraft] = useState<PracticeQuestionEditorValue>(() => createEmptyPracticeQuestionDraft())
const [questionImageUrl, setQuestionImageUrl] = useState("")
const [questionDetailById, setQuestionDetailById] = useState<Record<number, QuestionDetail>>({})
const [practiceTargetDelete, setPracticeTargetDelete] = useState<{ id: number; title: string } | null>(null)
const [questionTargetDelete, setQuestionTargetDelete] = useState<{ id: number; practiceId: number; text: string } | null>(null)
@ -597,7 +596,6 @@ export function HumanLanguagePage() {
const resetPracticeForm = () => setPracticeForm({ title: "", description: "", persona: "" })
const resetQuestionForm = () => {
setQuestionDraft(createEmptyPracticeQuestionDraft())
setQuestionImageUrl("")
}
const openCreatePracticeDialog = (subModuleId: number) => {
@ -680,7 +678,6 @@ export function HumanLanguagePage() {
return
}
setQuestionDetailById((prev) => ({ ...prev, [qid]: detail }))
setQuestionImageUrl(detail.image_url ?? "")
const sortedOpts = (detail.options ?? []).slice().sort((a, b) => a.option_order - b.option_order)
const shortAnswer =
Array.isArray(detail.short_answers) && detail.short_answers.length > 0
@ -738,6 +735,7 @@ export function HumanLanguagePage() {
sampleAnswerVoicePrompt: detail.sample_answer_voice_prompt ?? "",
audioCorrectAnswerText: detail.audio_correct_answer_text ?? "",
shortAnswer,
imageUrl: detail.image_url ?? "",
})
// Open only after the same form shape as create is fully populated (no empty-state flash).
setQuestionDialog({ open: true, mode: "edit", practiceId, questionId: qid })
@ -758,7 +756,7 @@ export function HumanLanguagePage() {
points: Number(d.points) || 1,
tips: d.tips.trim() || undefined,
explanation: d.explanation.trim() || undefined,
image_url: questionImageUrl.trim() || undefined,
image_url: d.imageUrl.trim() || undefined,
voice_prompt: d.voicePrompt.trim() || undefined,
sample_answer_voice_prompt: d.sampleAnswerVoicePrompt.trim() || undefined,
audio_correct_answer_text: d.audioCorrectAnswerText.trim() || undefined,
@ -1917,21 +1915,8 @@ export function HumanLanguagePage() {
}}
fieldErrors={questionFieldErrors}
showFieldErrors={questionSubmitAttempted || questionFormTouched}
mediaBusy={savingQuestion}
/>
<div className="mt-5 space-y-2 border-t border-grayScale-100 pt-5">
<label className="text-xs font-medium uppercase tracking-wider text-grayScale-500">Image URL (Optional)</label>
<Input
value={questionImageUrl}
onChange={(e) => {
setQuestionFormTouched(true)
setQuestionImageUrl(e.target.value)
}}
placeholder="https://…"
type="url"
className="h-11 font-mono text-[13px]"
/>
</div>
</Card>
</div>