Fix hydration mismatch and update defaults#666
Fix hydration mismatch and update defaults#666simonsobran wants to merge 5 commits intosrbhr:mainfrom
Conversation
There was a problem hiding this comment.
12 issues found across 18 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/frontend/components/builder/forms/generic-item-form.tsx">
<violation number="1" location="apps/frontend/components/builder/forms/generic-item-form.tsx:109">
P2: Prepending description entries while using index keys causes all existing items to shift keys. React will reuse RichTextEditor instances for different items, leading to state resets and performance issues. Use a stable key per description entry (e.g., store an id with each description) or avoid prepending if index keys are kept.</violation>
</file>
<file name="apps/backend/app/routers/config.py">
<violation number="1" location="apps/backend/app/routers/config.py:122">
P2: `string.Formatter().parse(prompt)` can raise `ValueError` for malformed format strings (e.g., unescaped `{`/`}`), and this exception is not caught. User-provided prompts with invalid braces will cause a 500 error instead of a 400 validation response.</violation>
<violation number="2" location="apps/backend/app/routers/config.py:457">
P2: update_prompt_template allows setting an empty prompt string, breaking the invariant enforced on creation and letting templates be saved with no prompt text.</violation>
</file>
<file name="apps/frontend/messages/zh.json">
<violation number="1" location="apps/frontend/messages/zh.json:45">
P3: Chinese locale file includes English strings for the new prompts feature, so users selecting zh will see untranslated UI text.</violation>
</file>
<file name="apps/backend/app/routers/resumes.py">
<violation number="1" location="apps/backend/app/routers/resumes.py:86">
P2: Synchronous TinyDB I/O was added inside _get_default_prompt_id(), which is called from async route handlers. This blocking DB call will freeze the event loop while reading prompt templates, reducing concurrency under load.</violation>
</file>
<file name="apps/frontend/messages/pt-BR.json">
<violation number="1" location="apps/frontend/messages/pt-BR.json:52">
P2: New pt-BR localization entries for the prompts feature are still in English, leading to a mixed-language UI for Portuguese users.</violation>
</file>
<file name="apps/frontend/messages/es.json">
<violation number="1" location="apps/frontend/messages/es.json:52">
P2: English strings were added to the Spanish locale file; these should be translated to Spanish to avoid mixed-language UI.</violation>
</file>
<file name="apps/backend/app/database.py">
<violation number="1" location="apps/backend/app/database.py:51">
P2: `reset_database` doesn’t clear the newly added `prompt_templates` table, so database resets leave custom prompts behind and can contaminate test/environment state.</violation>
</file>
<file name="apps/frontend/components/builder/forms/experience-form.tsx">
<violation number="1" location="apps/frontend/components/builder/forms/experience-form.tsx:67">
P2: Prepending new description items while rendering with `key={idx}` will shift all keys and cause RichTextEditor instances to be reused for the wrong content, risking state/cursor issues and unnecessary re-renders. Either append new items or use stable IDs for description entries.</violation>
</file>
<file name="apps/frontend/messages/ja.json">
<violation number="1" location="apps/frontend/messages/ja.json:51">
P2: The Japanese locale file adds a new `prompts` section with English strings, leaving UI labels untranslated for Japanese users.</violation>
</file>
<file name="apps/frontend/components/builder/forms/projects-form.tsx">
<violation number="1" location="apps/frontend/components/builder/forms/projects-form.tsx:68">
P2: Prepending description entries while using index keys forces React to remount all editors, causing state loss and unnecessary re-renders. Use stable keys or append instead of prepend.</violation>
</file>
<file name="apps/frontend/app/(default)/prompts/page.tsx">
<violation number="1" location="apps/frontend/app/(default)/prompts/page.tsx:76">
P2: Saving overwrites drafts with the server response even if the user edited the fields while the save request was in flight, so in-flight edits can be lost.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| items.map((item) => { | ||
| if (item.id === id) { | ||
| return { ...item, description: [...(item.description || []), ''] }; | ||
| return { ...item, description: ['', ...(item.description || [])] }; |
There was a problem hiding this comment.
P2: Prepending description entries while using index keys causes all existing items to shift keys. React will reuse RichTextEditor instances for different items, leading to state resets and performance issues. Use a stable key per description entry (e.g., store an id with each description) or avoid prepending if index keys are kept.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/builder/forms/generic-item-form.tsx, line 109:
<comment>Prepending description entries while using index keys causes all existing items to shift keys. React will reuse RichTextEditor instances for different items, leading to state resets and performance issues. Use a stable key per description entry (e.g., store an id with each description) or avoid prepending if index keys are kept.</comment>
<file context>
@@ -106,7 +106,7 @@ export const GenericItemForm: React.FC<GenericItemFormProps> = ({
items.map((item) => {
if (item.id === id) {
- return { ...item, description: [...(item.description || []), ''] };
+ return { ...item, description: ['', ...(item.description || [])] };
}
return item;
</file context>
| fields = { | ||
| field_name | ||
| for _, field_name, _, _ in string.Formatter().parse(prompt) | ||
| if field_name | ||
| } |
There was a problem hiding this comment.
P2: string.Formatter().parse(prompt) can raise ValueError for malformed format strings (e.g., unescaped {/}), and this exception is not caught. User-provided prompts with invalid braces will cause a 500 error instead of a 400 validation response.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/config.py, line 122:
<comment>`string.Formatter().parse(prompt)` can raise `ValueError` for malformed format strings (e.g., unescaped `{`/`}`), and this exception is not caught. User-provided prompts with invalid braces will cause a 500 error instead of a 400 validation response.</comment>
<file context>
@@ -67,7 +78,61 @@ def _mask_api_key(key: str) -> str:
+
+
+def _validate_prompt_template(prompt: str) -> None:
+ fields = {
+ field_name
+ for _, field_name, _, _ in string.Formatter().parse(prompt)
</file context>
| fields = { | |
| field_name | |
| for _, field_name, _, _ in string.Formatter().parse(prompt) | |
| if field_name | |
| } | |
| try: | |
| fields = { | |
| field_name | |
| for _, field_name, _, _ in string.Formatter().parse(prompt) | |
| if field_name | |
| } | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail="Invalid prompt format string.") from exc |
| prompt = request.prompt.strip() | ||
| if prompt: | ||
| _validate_prompt_template(prompt) | ||
| updates["prompt"] = prompt |
There was a problem hiding this comment.
P2: update_prompt_template allows setting an empty prompt string, breaking the invariant enforced on creation and letting templates be saved with no prompt text.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/config.py, line 457:
<comment>update_prompt_template allows setting an empty prompt string, breaking the invariant enforced on creation and letting templates be saved with no prompt text.</comment>
<file context>
@@ -320,6 +385,117 @@ async def update_prompt_config(
+ prompt = request.prompt.strip()
+ if prompt:
+ _validate_prompt_template(prompt)
+ updates["prompt"] = prompt
+
+ if not updates:
</file context>
| config = _load_config() | ||
| option_ids = {option["id"] for option in IMPROVE_PROMPT_OPTIONS} | ||
| option_ids.update( | ||
| prompt.get("prompt_id") for prompt in db.list_prompt_templates() if prompt.get("prompt_id") |
There was a problem hiding this comment.
P2: Synchronous TinyDB I/O was added inside _get_default_prompt_id(), which is called from async route handlers. This blocking DB call will freeze the event loop while reading prompt templates, reducing concurrency under load.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/resumes.py, line 86:
<comment>Synchronous TinyDB I/O was added inside _get_default_prompt_id(), which is called from async route handlers. This blocking DB call will freeze the event loop while reading prompt templates, reducing concurrency under load.</comment>
<file context>
@@ -82,6 +82,9 @@ def _get_default_prompt_id() -> str:
config = _load_config()
option_ids = {option["id"] for option in IMPROVE_PROMPT_OPTIONS}
+ option_ids.update(
+ prompt.get("prompt_id") for prompt in db.list_prompt_templates() if prompt.get("prompt_id")
+ )
prompt_id = config.get("default_prompt_id", DEFAULT_IMPROVE_PROMPT_ID)
</file context>
| }, | ||
| "prompts": { | ||
| "title": "Prompts", | ||
| "subtitle": "Manage tailoring intensity prompts", |
There was a problem hiding this comment.
P2: New pt-BR localization entries for the prompts feature are still in English, leading to a mixed-language UI for Portuguese users.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/messages/pt-BR.json, line 52:
<comment>New pt-BR localization entries for the prompts feature are still in English, leading to a mixed-language UI for Portuguese users.</comment>
<file context>
@@ -42,10 +42,58 @@
},
+ "prompts": {
+ "title": "Prompts",
+ "subtitle": "Manage tailoring intensity prompts",
+ "create": {
+ "title": "Create Prompt",
</file context>
| data.map((item) => { | ||
| if (item.id === id) { | ||
| return { ...item, description: [...(item.description || []), ''] }; | ||
| return { ...item, description: ['', ...(item.description || [])] }; |
There was a problem hiding this comment.
P2: Prepending new description items while rendering with key={idx} will shift all keys and cause RichTextEditor instances to be reused for the wrong content, risking state/cursor issues and unnecessary re-renders. Either append new items or use stable IDs for description entries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/builder/forms/experience-form.tsx, line 67:
<comment>Prepending new description items while rendering with `key={idx}` will shift all keys and cause RichTextEditor instances to be reused for the wrong content, risking state/cursor issues and unnecessary re-renders. Either append new items or use stable IDs for description entries.</comment>
<file context>
@@ -64,7 +64,7 @@ export const ExperienceForm: React.FC<ExperienceFormProps> = ({ data, onChange }
data.map((item) => {
if (item.id === id) {
- return { ...item, description: [...(item.description || []), ''] };
+ return { ...item, description: ['', ...(item.description || [])] };
}
return item;
</file context>
| return { ...item, description: ['', ...(item.description || [])] }; | |
| return { ...item, description: [...(item.description || []), ''] }; |
| "goToSettings": "設定へ" | ||
| }, | ||
| "prompts": { | ||
| "title": "Prompts", |
There was a problem hiding this comment.
P2: The Japanese locale file adds a new prompts section with English strings, leaving UI labels untranslated for Japanese users.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/messages/ja.json, line 51:
<comment>The Japanese locale file adds a new `prompts` section with English strings, leaving UI labels untranslated for Japanese users.</comment>
<file context>
@@ -42,10 +42,58 @@
"goToSettings": "設定へ"
},
+ "prompts": {
+ "title": "Prompts",
+ "subtitle": "Manage tailoring intensity prompts",
+ "create": {
</file context>
| data.map((item) => { | ||
| if (item.id === id) { | ||
| return { ...item, description: [...(item.description || []), ''] }; | ||
| return { ...item, description: ['', ...(item.description || [])] }; |
There was a problem hiding this comment.
P2: Prepending description entries while using index keys forces React to remount all editors, causing state loss and unnecessary re-renders. Use stable keys or append instead of prepend.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/builder/forms/projects-form.tsx, line 68:
<comment>Prepending description entries while using index keys forces React to remount all editors, causing state loss and unnecessary re-renders. Use stable keys or append instead of prepend.</comment>
<file context>
@@ -65,7 +65,7 @@ export const ProjectsForm: React.FC<ProjectsFormProps> = ({ data, onChange }) =>
data.map((item) => {
if (item.id === id) {
- return { ...item, description: [...(item.description || []), ''] };
+ return { ...item, description: ['', ...(item.description || [])] };
}
return item;
</file context>
| return { ...item, description: ['', ...(item.description || [])] }; | |
| return { ...item, description: [...(item.description || []), ''] }; |
| }, [loadData]); | ||
|
|
||
| const handleDraftChange = (id: string, update: Partial<PromptDraft>) => { | ||
| setDrafts((prev) => ({ |
There was a problem hiding this comment.
P2: Saving overwrites drafts with the server response even if the user edited the fields while the save request was in flight, so in-flight edits can be lost.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/app/(default)/prompts/page.tsx, line 76:
<comment>Saving overwrites drafts with the server response even if the user edited the fields while the save request was in flight, so in-flight edits can be lost.</comment>
<file context>
@@ -0,0 +1,446 @@
+ }, [loadData]);
+
+ const handleDraftChange = (id: string, update: Partial<PromptDraft>) => {
+ setDrafts((prev) => ({
+ ...prev,
+ [id]: { ...prev[id], ...update },
</file context>
| "dashboard": "仪表板", | ||
| "builder": "简历生成器", | ||
| "tailor": "定制简历", | ||
| "prompts": "Prompts", |
There was a problem hiding this comment.
P3: Chinese locale file includes English strings for the new prompts feature, so users selecting zh will see untranslated UI text.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/messages/zh.json, line 45:
<comment>Chinese locale file includes English strings for the new prompts feature, so users selecting zh will see untranslated UI text.</comment>
<file context>
@@ -42,10 +42,58 @@
"dashboard": "仪表板",
"builder": "简历生成器",
"tailor": "定制简历",
+ "prompts": "Prompts",
"settings": "设置",
"backToDashboard": "返回仪表板",
</file context>
There was a problem hiding this comment.
5 issues found across 20 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="apps/frontend/messages/ja.json">
<violation number="1" location="apps/frontend/messages/ja.json:362">
P3: The Japanese error message example uses the English term "Present" while the placeholder uses "現在", creating inconsistent guidance for users. Align the example with the localized term used elsewhere.</violation>
</file>
<file name="apps/frontend/components/builder/resume-builder.tsx">
<violation number="1" location="apps/frontend/components/builder/resume-builder.tsx:435">
P3: Download filename is built from placeholder resumeData before async fetch completes, so users can get incorrect filenames when downloading immediately after load.</violation>
</file>
<file name="apps/frontend/components/builder/forms/experience-form.tsx">
<violation number="1" location="apps/frontend/components/builder/forms/experience-form.tsx:76">
P2: Touched validation state persists across deletions and can be applied to newly added items when a deleted id is reused, causing the years validation warning to show prematurely.</violation>
</file>
<file name="apps/frontend/lib/utils/filename.ts">
<violation number="1" location="apps/frontend/lib/utils/filename.ts:9">
P2: Combining diacritic marks produced by NFKD normalization are treated as non-letter characters and replaced with underscores, so names like "Résumé" become "R_e_s_u_m_e_". If the intent is to remove accents, strip combining marks before replacing non-alphanumerics.</violation>
<violation number="2" location="apps/frontend/lib/utils/filename.ts:57">
P2: When the person name is missing, the filename contains duplicated "Cover_Letter" segments because it is used as both the fallback and the fixed second segment.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| export const ExperienceForm: React.FC<ExperienceFormProps> = ({ data, onChange }) => { | ||
| const { t } = useTranslations(); | ||
| const [isMounted, setIsMounted] = useState(false); | ||
| const [touchedYears, setTouchedYears] = useState<Record<number, boolean>>({}); |
There was a problem hiding this comment.
P2: Touched validation state persists across deletions and can be applied to newly added items when a deleted id is reused, causing the years validation warning to show prematurely.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/builder/forms/experience-form.tsx, line 76:
<comment>Touched validation state persists across deletions and can be applied to newly added items when a deleted id is reused, causing the years validation warning to show prematurely.</comment>
<file context>
@@ -1,24 +1,116 @@
export const ExperienceForm: React.FC<ExperienceFormProps> = ({ data, onChange }) => {
const { t } = useTranslations();
+ const [isMounted, setIsMounted] = useState(false);
+ const [touchedYears, setTouchedYears] = useState<Record<number, boolean>>({});
+ const monthPattern = useMemo(
+ () =>
</file context>
|
|
||
| function sanitizeSegment(value: string): string { | ||
| return value | ||
| .normalize('NFKD') |
There was a problem hiding this comment.
P2: Combining diacritic marks produced by NFKD normalization are treated as non-letter characters and replaced with underscores, so names like "Résumé" become "R_e_s_u_m_e_". If the intent is to remove accents, strip combining marks before replacing non-alphanumerics.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/lib/utils/filename.ts, line 9:
<comment>Combining diacritic marks produced by NFKD normalization are treated as non-letter characters and replaced with underscores, so names like "Résumé" become "R_e_s_u_m_e_". If the intent is to remove accents, strip combining marks before replacing non-alphanumerics.</comment>
<file context>
@@ -0,0 +1,65 @@
+
+function sanitizeSegment(value: string): string {
+ return value
+ .normalize('NFKD')
+ .replace(/[^\p{L}\p{N}]+/gu, '_')
+ .replace(/^_+|_+$/g, '')
</file context>
| ): string { | ||
| const name = getPersonName(resumeData); | ||
| const segments = [ | ||
| name ? sanitizeSegment(name) : 'Cover_Letter', |
There was a problem hiding this comment.
P2: When the person name is missing, the filename contains duplicated "Cover_Letter" segments because it is used as both the fallback and the fixed second segment.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/lib/utils/filename.ts, line 57:
<comment>When the person name is missing, the filename contains duplicated "Cover_Letter" segments because it is used as both the fallback and the fixed second segment.</comment>
<file context>
@@ -0,0 +1,65 @@
+): string {
+ const name = getPersonName(resumeData);
+ const segments = [
+ name ? sanitizeSegment(name) : 'Cover_Letter',
+ 'Cover_Letter',
+ locale ? sanitizeSegment(locale) : null,
</file context>
| "jobDescriptionOptional": "職務概要(任意)" | ||
| }, | ||
| "errors": { | ||
| "yearsRequiresMonth": "月名を含めてください(例: Aug 2020 - Present)" |
There was a problem hiding this comment.
P3: The Japanese error message example uses the English term "Present" while the placeholder uses "現在", creating inconsistent guidance for users. Align the example with the localized term used elsewhere.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/messages/ja.json, line 362:
<comment>The Japanese error message example uses the English term "Present" while the placeholder uses "現在", creating inconsistent guidance for users. Align the example with the localized term used elsewhere.</comment>
<file context>
@@ -352,15 +352,21 @@
+ "jobDescriptionOptional": "職務概要(任意)"
+ },
+ "errors": {
+ "yearsRequiresMonth": "月名を含めてください(例: Aug 2020 - Present)"
},
"placeholders": {
</file context>
| "yearsRequiresMonth": "月名を含めてください(例: Aug 2020 - Present)" | |
| "yearsRequiresMonth": "月名を含めてください(例: Aug 2020 - 現在)" |
| setIsDownloading(true); | ||
| const blob = await downloadResumePdf(resumeId, templateSettings, uiLanguage); | ||
| downloadBlobAsFile(blob, `resume_${resumeId}.pdf`); | ||
| downloadBlobAsFile(blob, buildResumeFilename(resumeData, templateSettings, uiLanguage)); |
There was a problem hiding this comment.
P3: Download filename is built from placeholder resumeData before async fetch completes, so users can get incorrect filenames when downloading immediately after load.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/builder/resume-builder.tsx, line 435:
<comment>Download filename is built from placeholder resumeData before async fetch completes, so users can get incorrect filenames when downloading immediately after load.</comment>
<file context>
@@ -431,7 +432,7 @@ const ResumeBuilderContent = () => {
setIsDownloading(true);
const blob = await downloadResumePdf(resumeId, templateSettings, uiLanguage);
- downloadBlobAsFile(blob, `resume_${resumeId}.pdf`);
+ downloadBlobAsFile(blob, buildResumeFilename(resumeData, templateSettings, uiLanguage));
showNotification(t('builder.alerts.downloadSuccess'), 'success');
} catch (error) {
</file context>
Pull Request Title
Related Issue
Description
Fixed hydration, added support for new gpt models with reasoning
Type
Proposed Changes
Screenshots / Code Snippets (if applicable)
How to Test
Checklist
Additional Information
copilot:walkthrough
Summary by cubic
Fixes hydration mismatch by enabling drag-and-drop only after mount across resume sections and experience items, updates defaults (compactMode=true, top/bottom margins=5), and adds editable tailoring prompts with backend CRUD and a Prompts page (create/edit/delete, set default; Tailor lists custom options). Also sets reasoning_effort="medium" for OpenAI o1/o3/gpt-5 and fixes TipTap remounts to prevent flicker.
Written for commit cb1237a. Summary will update on new commits.