Spaces:
Sleeping
Sleeping
[brand+ux] Rebrand to Fugee, render markdown, branded detailed documents
Browse files- Rebrand Refuge -> Fugee across all user-facing text (wordmark, site bar, page
title, agent persona, document headers). Word-boundary replace so the legal
term "Refugee" is untouched. Repo/package name stays "refuge".
- app/mdlite.py: tiny Markdown->HTML (bold/italic/code, #headings, -/1. lists,
paragraphs). Assessment reasoning + chat bubbles now render markdown instead of
showing literal ** and #.
- Documents: branded masthead (Fugee mark + wordmark), Fraunces/Inter web fonts,
page footer + page numbers, nicer headings/steps/contacts styling — for both
the PDF and (wordmark header) the editable Word files.
58 fast tests pass; verified PDF carries the brand + footer and markdown renders.
Co-authored-by: Codex <noreply@openai.com>
- agent/drafting.py +1 -1
- agent/tools/doc_generator.py +33 -10
- agent/tools/templates/_head.html +32 -14
- agent/tools/templates/action_plan.html +1 -1
- agent/tools/templates/emergency_contacts.html +2 -2
- agent/tools/templates/personal_statement.html +2 -2
- agent/tools/templates/rights_summary_card.html +1 -1
- app/app.py +3 -3
- app/mdlite.py +71 -0
- app/phases/assessment.py +12 -8
- app/phases/documents.py +1 -1
- app/phases/intake.py +2 -2
- app/phases/interview.py +2 -6
- app/phases/recommendations.py +2 -2
- app/prompts/system.md +2 -2
agent/drafting.py
CHANGED
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
| 12 |
from agent.events import ErrorEvent, TextDeltaEvent
|
| 13 |
from agent.loop import create_loop
|
| 14 |
|
| 15 |
-
_SYSTEM = """You are
|
| 16 |
to support an asylum / refugee claim. Write with dignity, in plain, clear language,
|
| 17 |
in {language}.
|
| 18 |
|
|
|
|
| 12 |
from agent.events import ErrorEvent, TextDeltaEvent
|
| 13 |
from agent.loop import create_loop
|
| 14 |
|
| 15 |
+
_SYSTEM = """You are Fugee, helping a person draft a first-person PERSONAL STATEMENT
|
| 16 |
to support an asylum / refugee claim. Write with dignity, in plain, clear language,
|
| 17 |
in {language}.
|
| 18 |
|
agent/tools/doc_generator.py
CHANGED
|
@@ -78,8 +78,31 @@ def _docx_add_rich(doc: Document, text: str):
|
|
| 78 |
run.font.color.rgb = _AMBER
|
| 79 |
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
def _new_docx(title: str, subtitle: str) -> Document:
|
| 82 |
doc = Document()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
h = doc.add_heading(title, level=0)
|
| 84 |
try:
|
| 85 |
h.runs[0].font.color.rgb = _TEAL
|
|
@@ -93,7 +116,7 @@ def _new_docx(title: str, subtitle: str) -> Document:
|
|
| 93 |
|
| 94 |
|
| 95 |
def _pdf_from_html(body_html: str, path: Path):
|
| 96 |
-
HTML(string=f"<!DOCTYPE html><html><head>{_HEAD}</head><body>{body_html}</body></html>").write_pdf(str(path))
|
| 97 |
|
| 98 |
|
| 99 |
# -- per-document content ---------------------------------------------------
|
|
@@ -101,7 +124,7 @@ def _pdf_from_html(body_html: str, path: Path):
|
|
| 101 |
def _statement_body_html(text: str) -> str:
|
| 102 |
return (
|
| 103 |
"<h1>Personal Statement</h1>"
|
| 104 |
-
'<p class="sub">In support of an application for refugee status · Prepared with
|
| 105 |
f"{_html_with_placeholders(text)}"
|
| 106 |
'<p class="note">Fields in amber are placeholders for you to complete. You can edit any part '
|
| 107 |
"of this statement in the Word (.docx) version.</p>"
|
|
@@ -150,7 +173,7 @@ def generate(session, statement: str | None = None, out_dir: Path | None = None)
|
|
| 150 |
# 1. Personal statement (LLM-drafted) — PDF + DOCX
|
| 151 |
ps_pdf, ps_docx = out_dir / "personal_statement.pdf", out_dir / "personal_statement.docx"
|
| 152 |
_pdf_from_html(_statement_body_html(stmt), ps_pdf)
|
| 153 |
-
d = _new_docx("Personal Statement", "In support of an application for refugee status · Prepared with
|
| 154 |
_docx_add_rich(d, stmt)
|
| 155 |
d.save(str(ps_docx))
|
| 156 |
docs.append(GeneratedDoc("personal_statement", "Personal statement (editable)",
|
|
@@ -159,13 +182,13 @@ def generate(session, statement: str | None = None, out_dir: Path | None = None)
|
|
| 159 |
# 2. Action plan
|
| 160 |
country, intro, steps = _action_plan_blocks(session, rec)
|
| 161 |
ap_html = (f"<h1>Action Plan — {html.escape(country)}</h1>"
|
| 162 |
-
'<p class="sub">Step-by-step roadmap · Prepared with
|
| 163 |
f"<p>{html.escape(intro)}</p>" +
|
| 164 |
"".join(f'<p class="step"><b>Step {i}.</b> {html.escape(str(s))}</p>'
|
| 165 |
for i, s in enumerate(steps, 1)))
|
| 166 |
ap_pdf, ap_docx = out_dir / "action_plan.pdf", out_dir / "action_plan.docx"
|
| 167 |
_pdf_from_html(ap_html, ap_pdf)
|
| 168 |
-
d = _new_docx(f"Action Plan — {country}", "Step-by-step roadmap · Prepared with
|
| 169 |
d.add_paragraph(intro)
|
| 170 |
for i, s in enumerate(steps, 1):
|
| 171 |
d.add_paragraph(f"Step {i}. {s}", style="List Number" if "List Number" in [s.name for s in d.styles] else None)
|
|
@@ -176,14 +199,14 @@ def generate(session, statement: str | None = None, out_dir: Path | None = None)
|
|
| 176 |
office = (rec or {}).get("unhcrOffice") or "Nearest UNHCR office"
|
| 177 |
orgs = _orgs(rec)
|
| 178 |
ec_html = ("<h1>Emergency Contacts</h1>"
|
| 179 |
-
'<p class="sub">UNHCR offices & legal aid · Prepared with
|
| 180 |
f"<h2>{html.escape(country)}</h2><p>UNHCR office: {html.escape(office)}</p>"
|
| 181 |
"<h2>Legal aid & support</h2>" +
|
| 182 |
("".join(f'<div class="org"><b>{html.escape(n)}</b><span>{html.escape(u)}</span></div>' for n, u in orgs)
|
| 183 |
or "<p>Ask the UNHCR office for registered legal-aid partners.</p>"))
|
| 184 |
ec_pdf, ec_docx = out_dir / "emergency_contacts.pdf", out_dir / "emergency_contacts.docx"
|
| 185 |
_pdf_from_html(ec_html, ec_pdf)
|
| 186 |
-
d = _new_docx("Emergency Contacts", "UNHCR offices & legal aid · Prepared with
|
| 187 |
d.add_paragraph(f"{country} — UNHCR office: {office}")
|
| 188 |
for n, u in orgs:
|
| 189 |
d.add_paragraph(f"{n} — {u}")
|
|
@@ -193,7 +216,7 @@ def generate(session, statement: str | None = None, out_dir: Path | None = None)
|
|
| 193 |
# 4. Rights summary card
|
| 194 |
grounds = ", ".join(session.assessment.convention_grounds) if session.assessment.convention_grounds else "to be confirmed"
|
| 195 |
rc_html = ("<h1>Your Rights — Summary Card</h1>"
|
| 196 |
-
'<p class="sub">Key protections · Prepared with
|
| 197 |
"<h2>Non-refoulement</h2><p>You cannot be forced back to a country where your life or freedom "
|
| 198 |
"would be at serious risk (1951 Refugee Convention).</p>"
|
| 199 |
"<h2>While your claim is decided</h2><ul>"
|
|
@@ -205,7 +228,7 @@ def generate(session, statement: str | None = None, out_dir: Path | None = None)
|
|
| 205 |
f"Seeking protection in: {html.escape(country)} · Grounds: {html.escape(grounds)}</p>")
|
| 206 |
rc_pdf, rc_docx = out_dir / "rights_summary_card.pdf", out_dir / "rights_summary_card.docx"
|
| 207 |
_pdf_from_html(rc_html, rc_pdf)
|
| 208 |
-
d = _new_docx("Your Rights — Summary Card", "Key protections · Prepared with
|
| 209 |
d.add_paragraph("Non-refoulement: you cannot be forced back to a country where your life or freedom "
|
| 210 |
"would be at serious risk (1951 Refugee Convention).")
|
| 211 |
for r in ["The right to seek asylum and a fair hearing.",
|
|
@@ -241,7 +264,7 @@ def preview_statement_html(session, statement: str | None = None) -> str:
|
|
| 241 |
"""Head-less personal-statement snippet for the on-screen preview."""
|
| 242 |
stmt = statement or fallback_statement(session)
|
| 243 |
return ('<article class="doc"><h4>Personal Statement</h4>'
|
| 244 |
-
'<p class="doc__sub">In support of an application for refugee status · Prepared with
|
| 245 |
f"{_html_with_placeholders(stmt)}</article>")
|
| 246 |
|
| 247 |
|
|
|
|
| 78 |
run.font.color.rgb = _AMBER
|
| 79 |
|
| 80 |
|
| 81 |
+
# Branded masthead (the Fugee mark + wordmark) shown atop every PDF.
|
| 82 |
+
_LOGO_SVG = (
|
| 83 |
+
'<svg class="logo" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">'
|
| 84 |
+
'<circle cx="16" cy="16" r="15.5" fill="#0E6A58"/>'
|
| 85 |
+
'<path d="M16 7l7 6.5V25h-4.6v-6.2h-4.8V25H9V13.5L16 7z" fill="#fff"/>'
|
| 86 |
+
'<circle cx="16" cy="13.6" r="1.7" fill="#E07B39"/></svg>'
|
| 87 |
+
)
|
| 88 |
+
_BRAND = (
|
| 89 |
+
f'<div class="brand">{_LOGO_SVG}<span class="name">Fugee</span>'
|
| 90 |
+
'<span class="tag">Safe guidance for people on the move</span></div>'
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
def _new_docx(title: str, subtitle: str) -> Document:
|
| 95 |
doc = Document()
|
| 96 |
+
# Branded wordmark + rule (a logo image would need a raster asset; the
|
| 97 |
+
# wordmark keeps the package self-contained).
|
| 98 |
+
brand = doc.add_paragraph()
|
| 99 |
+
run = brand.add_run("Fugee")
|
| 100 |
+
run.bold = True
|
| 101 |
+
run.font.size = Pt(20)
|
| 102 |
+
run.font.color.rgb = _TEAL
|
| 103 |
+
tag = brand.add_run(" Safe guidance for people on the move")
|
| 104 |
+
tag.font.size = Pt(9)
|
| 105 |
+
tag.font.color.rgb = RGBColor(0x6B, 0x72, 0x80)
|
| 106 |
h = doc.add_heading(title, level=0)
|
| 107 |
try:
|
| 108 |
h.runs[0].font.color.rgb = _TEAL
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
def _pdf_from_html(body_html: str, path: Path):
|
| 119 |
+
HTML(string=f"<!DOCTYPE html><html><head>{_HEAD}</head><body>{_BRAND}{body_html}</body></html>").write_pdf(str(path))
|
| 120 |
|
| 121 |
|
| 122 |
# -- per-document content ---------------------------------------------------
|
|
|
|
| 124 |
def _statement_body_html(text: str) -> str:
|
| 125 |
return (
|
| 126 |
"<h1>Personal Statement</h1>"
|
| 127 |
+
'<p class="sub">In support of an application for refugee status · Prepared with Fugee</p>'
|
| 128 |
f"{_html_with_placeholders(text)}"
|
| 129 |
'<p class="note">Fields in amber are placeholders for you to complete. You can edit any part '
|
| 130 |
"of this statement in the Word (.docx) version.</p>"
|
|
|
|
| 173 |
# 1. Personal statement (LLM-drafted) — PDF + DOCX
|
| 174 |
ps_pdf, ps_docx = out_dir / "personal_statement.pdf", out_dir / "personal_statement.docx"
|
| 175 |
_pdf_from_html(_statement_body_html(stmt), ps_pdf)
|
| 176 |
+
d = _new_docx("Personal Statement", "In support of an application for refugee status · Prepared with Fugee")
|
| 177 |
_docx_add_rich(d, stmt)
|
| 178 |
d.save(str(ps_docx))
|
| 179 |
docs.append(GeneratedDoc("personal_statement", "Personal statement (editable)",
|
|
|
|
| 182 |
# 2. Action plan
|
| 183 |
country, intro, steps = _action_plan_blocks(session, rec)
|
| 184 |
ap_html = (f"<h1>Action Plan — {html.escape(country)}</h1>"
|
| 185 |
+
'<p class="sub">Step-by-step roadmap · Prepared with Fugee</p>'
|
| 186 |
f"<p>{html.escape(intro)}</p>" +
|
| 187 |
"".join(f'<p class="step"><b>Step {i}.</b> {html.escape(str(s))}</p>'
|
| 188 |
for i, s in enumerate(steps, 1)))
|
| 189 |
ap_pdf, ap_docx = out_dir / "action_plan.pdf", out_dir / "action_plan.docx"
|
| 190 |
_pdf_from_html(ap_html, ap_pdf)
|
| 191 |
+
d = _new_docx(f"Action Plan — {country}", "Step-by-step roadmap · Prepared with Fugee")
|
| 192 |
d.add_paragraph(intro)
|
| 193 |
for i, s in enumerate(steps, 1):
|
| 194 |
d.add_paragraph(f"Step {i}. {s}", style="List Number" if "List Number" in [s.name for s in d.styles] else None)
|
|
|
|
| 199 |
office = (rec or {}).get("unhcrOffice") or "Nearest UNHCR office"
|
| 200 |
orgs = _orgs(rec)
|
| 201 |
ec_html = ("<h1>Emergency Contacts</h1>"
|
| 202 |
+
'<p class="sub">UNHCR offices & legal aid · Prepared with Fugee</p>'
|
| 203 |
f"<h2>{html.escape(country)}</h2><p>UNHCR office: {html.escape(office)}</p>"
|
| 204 |
"<h2>Legal aid & support</h2>" +
|
| 205 |
("".join(f'<div class="org"><b>{html.escape(n)}</b><span>{html.escape(u)}</span></div>' for n, u in orgs)
|
| 206 |
or "<p>Ask the UNHCR office for registered legal-aid partners.</p>"))
|
| 207 |
ec_pdf, ec_docx = out_dir / "emergency_contacts.pdf", out_dir / "emergency_contacts.docx"
|
| 208 |
_pdf_from_html(ec_html, ec_pdf)
|
| 209 |
+
d = _new_docx("Emergency Contacts", "UNHCR offices & legal aid · Prepared with Fugee")
|
| 210 |
d.add_paragraph(f"{country} — UNHCR office: {office}")
|
| 211 |
for n, u in orgs:
|
| 212 |
d.add_paragraph(f"{n} — {u}")
|
|
|
|
| 216 |
# 4. Rights summary card
|
| 217 |
grounds = ", ".join(session.assessment.convention_grounds) if session.assessment.convention_grounds else "to be confirmed"
|
| 218 |
rc_html = ("<h1>Your Rights — Summary Card</h1>"
|
| 219 |
+
'<p class="sub">Key protections · Prepared with Fugee</p>'
|
| 220 |
"<h2>Non-refoulement</h2><p>You cannot be forced back to a country where your life or freedom "
|
| 221 |
"would be at serious risk (1951 Refugee Convention).</p>"
|
| 222 |
"<h2>While your claim is decided</h2><ul>"
|
|
|
|
| 228 |
f"Seeking protection in: {html.escape(country)} · Grounds: {html.escape(grounds)}</p>")
|
| 229 |
rc_pdf, rc_docx = out_dir / "rights_summary_card.pdf", out_dir / "rights_summary_card.docx"
|
| 230 |
_pdf_from_html(rc_html, rc_pdf)
|
| 231 |
+
d = _new_docx("Your Rights — Summary Card", "Key protections · Prepared with Fugee")
|
| 232 |
d.add_paragraph("Non-refoulement: you cannot be forced back to a country where your life or freedom "
|
| 233 |
"would be at serious risk (1951 Refugee Convention).")
|
| 234 |
for r in ["The right to seek asylum and a fair hearing.",
|
|
|
|
| 264 |
"""Head-less personal-statement snippet for the on-screen preview."""
|
| 265 |
stmt = statement or fallback_statement(session)
|
| 266 |
return ('<article class="doc"><h4>Personal Statement</h4>'
|
| 267 |
+
'<p class="doc__sub">In support of an application for refugee status · Prepared with Fugee</p>'
|
| 268 |
f"{_html_with_placeholders(stmt)}</article>")
|
| 269 |
|
| 270 |
|
agent/tools/templates/_head.html
CHANGED
|
@@ -1,30 +1,48 @@
|
|
| 1 |
<meta charset="utf-8" />
|
| 2 |
<style>
|
| 3 |
-
@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
:root {
|
| 5 |
-
--primary: #0E6A58; --primary-deep: #0A5042; --accent-tint: #FBEDE1;
|
| 6 |
-
--text: #1A1A1A; --muted: #6B7280; --line: #E7E2D8;
|
| 7 |
}
|
| 8 |
* { box-sizing: border-box; }
|
| 9 |
body { font-family: "Inter", "Helvetica Neue", Arial, sans-serif; color: var(--text);
|
| 10 |
-
font-size:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
h1, h2, h3, h4 { font-family: "Fraunces", Georgia, "Times New Roman", serif; color: var(--primary-deep);
|
| 12 |
margin: 0 0 4px; line-height: 1.2; }
|
| 13 |
-
h1 { font-size:
|
| 14 |
-
h2 { font-size:
|
| 15 |
-
.sub { color: var(--muted); font-size: 10pt; margin: 0 0
|
| 16 |
p { margin: 0 0 11px; }
|
| 17 |
.fill { background: var(--accent-tint); color: #7a431a; padding: 0 4px; border-radius: 3px; font-weight: 600; }
|
| 18 |
-
.blank { display: inline-block; min-width:
|
| 19 |
-
.divider { height: 1px; background: var(--line); margin:
|
| 20 |
-
.note { color: var(--muted); font-size: 9.5pt; margin-top:
|
| 21 |
ul, ol { margin: 0 0 11px; padding-left: 20px; }
|
| 22 |
li { margin-bottom: 6px; }
|
| 23 |
-
.step { margin-bottom:
|
| 24 |
.step b { color: var(--primary-deep); }
|
| 25 |
-
.chip { display: inline-block; background:
|
| 26 |
-
padding: 2px
|
| 27 |
.org { margin-bottom: 8px; }
|
| 28 |
-
.org b { display: block; }
|
| 29 |
.org span { color: var(--muted); font-size: 10pt; }
|
| 30 |
</style>
|
|
|
|
| 1 |
<meta charset="utf-8" />
|
| 2 |
<style>
|
| 3 |
+
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap');
|
| 4 |
+
@page {
|
| 5 |
+
size: A4; margin: 24mm 20mm 20mm;
|
| 6 |
+
@bottom-center {
|
| 7 |
+
content: "Prepared with Fugee · Safe guidance for people on the move";
|
| 8 |
+
font-family: "Inter", sans-serif; font-size: 8pt; color: #9aa0ab;
|
| 9 |
+
}
|
| 10 |
+
@bottom-right { content: "Page " counter(page) " of " counter(pages);
|
| 11 |
+
font-family: "Inter", sans-serif; font-size: 8pt; color: #9aa0ab; }
|
| 12 |
+
}
|
| 13 |
:root {
|
| 14 |
+
--primary: #0E6A58; --primary-deep: #0A5042; --accent: #E07B39; --accent-tint: #FBEDE1;
|
| 15 |
+
--text: #1A1A1A; --muted: #6B7280; --line: #E7E2D8; --tint: #E5EFEA;
|
| 16 |
}
|
| 17 |
* { box-sizing: border-box; }
|
| 18 |
body { font-family: "Inter", "Helvetica Neue", Arial, sans-serif; color: var(--text);
|
| 19 |
+
font-size: 11.5pt; line-height: 1.65; }
|
| 20 |
+
|
| 21 |
+
/* Branded masthead */
|
| 22 |
+
.brand { display: flex; align-items: center; gap: 10px; padding-bottom: 12px;
|
| 23 |
+
border-bottom: 2px solid var(--primary); margin-bottom: 22px; }
|
| 24 |
+
.brand .logo { width: 30px; height: 30px; flex: 0 0 auto; }
|
| 25 |
+
.brand .name { font-family: "Fraunces", Georgia, serif; font-weight: 700; font-size: 19pt;
|
| 26 |
+
color: var(--primary-deep); letter-spacing: -.01em; }
|
| 27 |
+
.brand .tag { margin-left: auto; font-size: 8.5pt; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
|
| 28 |
+
|
| 29 |
h1, h2, h3, h4 { font-family: "Fraunces", Georgia, "Times New Roman", serif; color: var(--primary-deep);
|
| 30 |
margin: 0 0 4px; line-height: 1.2; }
|
| 31 |
+
h1 { font-size: 23pt; }
|
| 32 |
+
h2 { font-size: 14pt; margin-top: 20px; padding-bottom: 3px; border-bottom: 1px solid var(--line); }
|
| 33 |
+
.sub { color: var(--muted); font-size: 10pt; margin: 0 0 18px; letter-spacing: .02em; }
|
| 34 |
p { margin: 0 0 11px; }
|
| 35 |
.fill { background: var(--accent-tint); color: #7a431a; padding: 0 4px; border-radius: 3px; font-weight: 600; }
|
| 36 |
+
.blank { display: inline-block; min-width: 130px; border-bottom: 1px solid #999; }
|
| 37 |
+
.divider { height: 1px; background: var(--line); margin: 16px 0; }
|
| 38 |
+
.note { color: var(--muted); font-size: 9.5pt; margin-top: 22px; border-top: 1px solid var(--line); padding-top: 8px; }
|
| 39 |
ul, ol { margin: 0 0 11px; padding-left: 20px; }
|
| 40 |
li { margin-bottom: 6px; }
|
| 41 |
+
.step { margin-bottom: 14px; padding-left: 14px; border-left: 3px solid var(--tint); }
|
| 42 |
.step b { color: var(--primary-deep); }
|
| 43 |
+
.chip { display: inline-block; background: var(--tint); color: var(--primary-deep); font-size: 9.5pt;
|
| 44 |
+
padding: 2px 9px; border-radius: 999px; margin-right: 4px; }
|
| 45 |
.org { margin-bottom: 8px; }
|
| 46 |
+
.org b { display: block; color: var(--primary-deep); }
|
| 47 |
.org span { color: var(--muted); font-size: 10pt; }
|
| 48 |
</style>
|
agent/tools/templates/action_plan.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Action Plan — %%selected_country%%</h1>
|
| 4 |
-
<p class="sub">A step-by-step roadmap to seek asylum · Prepared with
|
| 5 |
|
| 6 |
<p>This plan is for seeking protection in %%selected_country%%. Processing typically
|
| 7 |
takes %%processing%%. UNHCR presence: %%unhcr_office%%.</p>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Action Plan — %%selected_country%%</h1>
|
| 4 |
+
<p class="sub">A step-by-step roadmap to seek asylum · Prepared with Fugee</p>
|
| 5 |
|
| 6 |
<p>This plan is for seeking protection in %%selected_country%%. Processing typically
|
| 7 |
takes %%processing%%. UNHCR presence: %%unhcr_office%%.</p>
|
agent/tools/templates/emergency_contacts.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Emergency Contacts</h1>
|
| 4 |
-
<p class="sub">UNHCR offices & legal aid · Prepared with
|
| 5 |
|
| 6 |
<h2>%%selected_country%%</h2>
|
| 7 |
<p>UNHCR office: %%unhcr_office%%</p>
|
|
@@ -17,6 +17,6 @@
|
|
| 17 |
<li>You have the right to request a translator and free legal assistance.</li>
|
| 18 |
</ul>
|
| 19 |
|
| 20 |
-
<p class="note">Contacts are drawn from the curated
|
| 21 |
Verify details locally where possible.</p>
|
| 22 |
</body></html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Emergency Contacts</h1>
|
| 4 |
+
<p class="sub">UNHCR offices & legal aid · Prepared with Fugee</p>
|
| 5 |
|
| 6 |
<h2>%%selected_country%%</h2>
|
| 7 |
<p>UNHCR office: %%unhcr_office%%</p>
|
|
|
|
| 17 |
<li>You have the right to request a translator and free legal assistance.</li>
|
| 18 |
</ul>
|
| 19 |
|
| 20 |
+
<p class="note">Contacts are drawn from the curated Fugee country reference.
|
| 21 |
Verify details locally where possible.</p>
|
| 22 |
</body></html>
|
agent/tools/templates/personal_statement.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Personal Statement</h1>
|
| 4 |
-
<p class="sub">In support of an application for refugee status · Prepared with
|
| 5 |
|
| 6 |
<p>My name is %%full_name%%. I am a national of %%origin_country%%. I am currently
|
| 7 |
in %%current_country%%%%family_clause%%.</p>
|
|
@@ -16,6 +16,6 @@
|
|
| 16 |
circumstances described above. I am asking for protection and the right not to be
|
| 17 |
returned to a place where my life or freedom would be threatened.</p>
|
| 18 |
|
| 19 |
-
<p class="note">Fields highlighted in amber were pre-filled from what you told
|
| 20 |
Anything left blank is for you to complete. You can edit any part of this statement.</p>
|
| 21 |
</body></html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Personal Statement</h1>
|
| 4 |
+
<p class="sub">In support of an application for refugee status · Prepared with Fugee</p>
|
| 5 |
|
| 6 |
<p>My name is %%full_name%%. I am a national of %%origin_country%%. I am currently
|
| 7 |
in %%current_country%%%%family_clause%%.</p>
|
|
|
|
| 16 |
circumstances described above. I am asking for protection and the right not to be
|
| 17 |
returned to a place where my life or freedom would be threatened.</p>
|
| 18 |
|
| 19 |
+
<p class="note">Fields highlighted in amber were pre-filled from what you told Fugee.
|
| 20 |
Anything left blank is for you to complete. You can edit any part of this statement.</p>
|
| 21 |
</body></html>
|
agent/tools/templates/rights_summary_card.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Your Rights — Summary Card</h1>
|
| 4 |
-
<p class="sub">Key protections for people seeking asylum · Prepared with
|
| 5 |
|
| 6 |
<h2>Non-refoulement</h2>
|
| 7 |
<p>You cannot be forced back to a country where your life or freedom would be at
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en"><head>%%HEAD%%</head><body>
|
| 3 |
<h1>Your Rights — Summary Card</h1>
|
| 4 |
+
<p class="sub">Key protections for people seeking asylum · Prepared with Fugee</p>
|
| 5 |
|
| 6 |
<h2>Non-refoulement</h2>
|
| 7 |
<p>You cannot be forced back to a country where your life or freedom would be at
|
app/app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""app/app.py —
|
| 2 |
|
| 3 |
Run locally with::
|
| 4 |
|
|
@@ -110,7 +110,7 @@ _LOGO_SVG = (
|
|
| 110 |
SITE_BAR_HTML = f"""
|
| 111 |
<header id="site-bar"><div class="inner">
|
| 112 |
{_LOGO_SVG}
|
| 113 |
-
<span class="name">
|
| 114 |
<span class="tag">Safe guidance for people on the move</span>
|
| 115 |
<nav class="nav"><span>Intake</span><span>Interview</span><span>Assessment</span>
|
| 116 |
<span>Recommendations</span><span>Documents</span></nav>
|
|
@@ -133,7 +133,7 @@ def _ready_for_assessment(session) -> bool:
|
|
| 133 |
|
| 134 |
|
| 135 |
def build_app() -> gr.Blocks:
|
| 136 |
-
with gr.Blocks(title="
|
| 137 |
gr.HTML(SITE_BAR_HTML)
|
| 138 |
# Session + loop state shared across phases so each screen sees the same
|
| 139 |
# session object.
|
|
|
|
| 1 |
+
"""app/app.py — Fugee Gradio entrypoint.
|
| 2 |
|
| 3 |
Run locally with::
|
| 4 |
|
|
|
|
| 110 |
SITE_BAR_HTML = f"""
|
| 111 |
<header id="site-bar"><div class="inner">
|
| 112 |
{_LOGO_SVG}
|
| 113 |
+
<span class="name">Fugee</span>
|
| 114 |
<span class="tag">Safe guidance for people on the move</span>
|
| 115 |
<nav class="nav"><span>Intake</span><span>Interview</span><span>Assessment</span>
|
| 116 |
<span>Recommendations</span><span>Documents</span></nav>
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
def build_app() -> gr.Blocks:
|
| 136 |
+
with gr.Blocks(title="Fugee", analytics_enabled=False) as demo:
|
| 137 |
gr.HTML(SITE_BAR_HTML)
|
| 138 |
# Session + loop state shared across phases so each screen sees the same
|
| 139 |
# session object.
|
app/mdlite.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""app/mdlite.py — tiny, dependency-free Markdown → HTML for streamed model text.
|
| 2 |
+
|
| 3 |
+
Handles what small models actually emit: **bold**, *italic*, `code`, # headings,
|
| 4 |
+
- / * bullet lists, 1. numbered lists, and blank-line paragraphs. Everything is
|
| 5 |
+
HTML-escaped first, so it is safe to render model output.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import html
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
_BOLD = re.compile(r"\*\*(.+?)\*\*")
|
| 14 |
+
_ITALIC = re.compile(r"(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)")
|
| 15 |
+
_CODE = re.compile(r"`([^`]+)`")
|
| 16 |
+
_H = re.compile(r"^(#{1,6})\s+(.*)$")
|
| 17 |
+
_UL = re.compile(r"^\s*[-*]\s+(.*)$")
|
| 18 |
+
_OL = re.compile(r"^\s*\d+[.)]\s+(.*)$")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def inline(text: str) -> str:
|
| 22 |
+
s = html.escape(text)
|
| 23 |
+
s = _BOLD.sub(r"<strong>\1</strong>", s)
|
| 24 |
+
s = _ITALIC.sub(r"<em>\1</em>", s)
|
| 25 |
+
s = _CODE.sub(r"<code>\1</code>", s)
|
| 26 |
+
return s
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def md_to_html(text: str) -> str:
|
| 30 |
+
text = (text or "").strip()
|
| 31 |
+
if not text:
|
| 32 |
+
return ""
|
| 33 |
+
lines = text.split("\n")
|
| 34 |
+
out: list[str] = []
|
| 35 |
+
i = 0
|
| 36 |
+
while i < len(lines):
|
| 37 |
+
line = lines[i].rstrip()
|
| 38 |
+
if not line.strip():
|
| 39 |
+
i += 1
|
| 40 |
+
continue
|
| 41 |
+
h = _H.match(line)
|
| 42 |
+
if h:
|
| 43 |
+
out.append(f"<h4>{inline(h.group(2))}</h4>")
|
| 44 |
+
i += 1
|
| 45 |
+
continue
|
| 46 |
+
if _UL.match(line):
|
| 47 |
+
items = []
|
| 48 |
+
while i < len(lines) and _UL.match(lines[i]):
|
| 49 |
+
items.append(f"<li>{inline(_UL.match(lines[i]).group(1))}</li>")
|
| 50 |
+
i += 1
|
| 51 |
+
out.append("<ul>" + "".join(items) + "</ul>")
|
| 52 |
+
continue
|
| 53 |
+
if _OL.match(line):
|
| 54 |
+
items = []
|
| 55 |
+
while i < len(lines) and _OL.match(lines[i]):
|
| 56 |
+
items.append(f"<li>{inline(_OL.match(lines[i]).group(1))}</li>")
|
| 57 |
+
i += 1
|
| 58 |
+
out.append("<ol>" + "".join(items) + "</ol>")
|
| 59 |
+
continue
|
| 60 |
+
para = [line]
|
| 61 |
+
i += 1
|
| 62 |
+
while i < len(lines) and lines[i].strip() and not (
|
| 63 |
+
_H.match(lines[i]) or _UL.match(lines[i]) or _OL.match(lines[i])
|
| 64 |
+
):
|
| 65 |
+
para.append(lines[i].rstrip())
|
| 66 |
+
i += 1
|
| 67 |
+
out.append("<p>" + inline(" ".join(para)) + "</p>")
|
| 68 |
+
return "\n".join(out)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
__all__ = ["md_to_html", "inline"]
|
app/phases/assessment.py
CHANGED
|
@@ -21,6 +21,7 @@ from agent.tools.asylum_stats import asylum_stats_tool
|
|
| 21 |
from agent.tools.country_lookup import country_lookup_tool, lookup_country
|
| 22 |
from agent.tools.guideline_search import guideline_search_tool
|
| 23 |
from app.assessment_parse import parse_assessment
|
|
|
|
| 24 |
from app.phases.interview import advance_to
|
| 25 |
from app.prompt_loader import compose
|
| 26 |
from app.state.session import SessionState, State
|
|
@@ -68,10 +69,15 @@ ASSESSMENT_CSS = """
|
|
| 68 |
background:linear-gradient(90deg,var(--primary),var(--secondary)); transition:width .6s cubic-bezier(.4,0,.1,1); }
|
| 69 |
.assess .tool-chip { display:inline-flex; align-items:center; gap:7px; margin-top:10px; font-size:12.5px;
|
| 70 |
font-weight:600; color:var(--primary-deep); background:var(--primary-tint); padding:5px 11px; border-radius:var(--r-full); }
|
| 71 |
-
.reason-doc { font-size:14.5px; line-height:1.
|
| 72 |
-
.reason-doc
|
| 73 |
-
.reason-doc
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
#assess-proceed-row { padding:18px clamp(18px,4vw,28px); border-top:1px solid var(--line); justify-content:flex-end; }
|
| 76 |
#assess-proceed, #assess-proceed button { background:var(--accent) !important; color:var(--on-accent) !important;
|
| 77 |
box-shadow:0 2px 0 var(--accent-deep) !important; border:0 !important; font-weight:600 !important;
|
|
@@ -113,10 +119,8 @@ def render_facts(session: SessionState) -> str:
|
|
| 113 |
def render_reason(text: str) -> str:
|
| 114 |
visible, _ = parse_assessment(text)
|
| 115 |
if not visible.strip():
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
lns = "".join(f'<span class="ln">{html.escape(p)}</span>' for p in paras)
|
| 119 |
-
return f'<div class="reason-doc">{lns}</div>'
|
| 120 |
|
| 121 |
|
| 122 |
def render_progress(pct: int, status: str = "") -> str:
|
|
|
|
| 21 |
from agent.tools.country_lookup import country_lookup_tool, lookup_country
|
| 22 |
from agent.tools.guideline_search import guideline_search_tool
|
| 23 |
from app.assessment_parse import parse_assessment
|
| 24 |
+
from app.mdlite import md_to_html
|
| 25 |
from app.phases.interview import advance_to
|
| 26 |
from app.prompt_loader import compose
|
| 27 |
from app.state.session import SessionState, State
|
|
|
|
| 69 |
background:linear-gradient(90deg,var(--primary),var(--secondary)); transition:width .6s cubic-bezier(.4,0,.1,1); }
|
| 70 |
.assess .tool-chip { display:inline-flex; align-items:center; gap:7px; margin-top:10px; font-size:12.5px;
|
| 71 |
font-weight:600; color:var(--primary-deep); background:var(--primary-tint); padding:5px 11px; border-radius:var(--r-full); }
|
| 72 |
+
.reason-doc { font-size:14.5px; line-height:1.7; color:var(--text-secondary); }
|
| 73 |
+
.reason-doc p { margin:0 0 12px; }
|
| 74 |
+
.reason-doc h4 { font-family:var(--font-ui); font-size:14px; font-weight:700; color:var(--text);
|
| 75 |
+
margin:18px 0 8px; letter-spacing:.01em; }
|
| 76 |
+
.reason-doc strong { color:var(--text); font-weight:600; }
|
| 77 |
+
.reason-doc em { font-style:italic; }
|
| 78 |
+
.reason-doc ul, .reason-doc ol { margin:0 0 12px; padding-left:20px; }
|
| 79 |
+
.reason-doc li { margin-bottom:6px; }
|
| 80 |
+
.reason-doc code { background:var(--primary-tint); padding:1px 5px; border-radius:4px; font-size:13px; }
|
| 81 |
#assess-proceed-row { padding:18px clamp(18px,4vw,28px); border-top:1px solid var(--line); justify-content:flex-end; }
|
| 82 |
#assess-proceed, #assess-proceed button { background:var(--accent) !important; color:var(--on-accent) !important;
|
| 83 |
box-shadow:0 2px 0 var(--accent-deep) !important; border:0 !important; font-weight:600 !important;
|
|
|
|
| 119 |
def render_reason(text: str) -> str:
|
| 120 |
visible, _ = parse_assessment(text)
|
| 121 |
if not visible.strip():
|
| 122 |
+
return '<div class="reason-doc"><p>Beginning your assessment…</p></div>'
|
| 123 |
+
return f'<div class="reason-doc">{md_to_html(visible)}</div>'
|
|
|
|
|
|
|
| 124 |
|
| 125 |
|
| 126 |
def render_progress(pct: int, status: str = "") -> str:
|
app/phases/documents.py
CHANGED
|
@@ -19,7 +19,7 @@ from app.state.session import SessionState
|
|
| 19 |
|
| 20 |
# Evidence checklist: (label, sublabel, document_available key or None).
|
| 21 |
CHECKLIST = [
|
| 22 |
-
("Personal statement", "Drafted by
|
| 23 |
("Identity document or sworn statement", "Passport, national ID, or a sworn statement", "passport"),
|
| 24 |
("Proof of journey / entry", "Tickets, stamps, or a written account", "travel"),
|
| 25 |
("Photos or messages showing the threat", "Optional, but strengthens your claim", "evidence"),
|
|
|
|
| 19 |
|
| 20 |
# Evidence checklist: (label, sublabel, document_available key or None).
|
| 21 |
CHECKLIST = [
|
| 22 |
+
("Personal statement", "Drafted by Fugee · ready", "_always"),
|
| 23 |
("Identity document or sworn statement", "Passport, national ID, or a sworn statement", "passport"),
|
| 24 |
("Proof of journey / entry", "Tickets, stamps, or a written account", "travel"),
|
| 25 |
("Photos or messages showing the threat", "Optional, but strengthens your claim", "evidence"),
|
app/phases/intake.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""app/phases/intake.py — Phase 1 intake + language selection (T030-T032).
|
| 2 |
|
| 3 |
-
Matches mockup.html #phase-1: a calm welcome with the
|
| 4 |
topographic backdrop, a grid of language pills each in its own script, an amber
|
| 5 |
"Begin in <language>" primary action, and a persistent privacy trust note.
|
| 6 |
|
|
@@ -100,7 +100,7 @@ _HEADER_HTML = f"""
|
|
| 100 |
<div class="intake-logo" aria-hidden="true">
|
| 101 |
<svg width="64" height="64" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="15.5" fill="#0E6A58"/><circle cx="16" cy="16" r="15.5" stroke="#0A5042"/><path d="M16 7l7 6.5V25h-4.6v-6.2h-4.8V25H9V13.5L16 7z" fill="#fff"/><circle cx="16" cy="13.6" r="1.7" fill="#E07B39"/></svg>
|
| 102 |
</div>
|
| 103 |
-
<h1 class="intake-h1">
|
| 104 |
<p class="intake-tagline">Safe guidance for people on the move</p>
|
| 105 |
<p class="intake-lbl">Choose your language</p>
|
| 106 |
</div>
|
|
|
|
| 1 |
"""app/phases/intake.py — Phase 1 intake + language selection (T030-T032).
|
| 2 |
|
| 3 |
+
Matches mockup.html #phase-1: a calm welcome with the Fugee wordmark, a
|
| 4 |
topographic backdrop, a grid of language pills each in its own script, an amber
|
| 5 |
"Begin in <language>" primary action, and a persistent privacy trust note.
|
| 6 |
|
|
|
|
| 100 |
<div class="intake-logo" aria-hidden="true">
|
| 101 |
<svg width="64" height="64" viewBox="0 0 32 32" fill="none"><circle cx="16" cy="16" r="15.5" fill="#0E6A58"/><circle cx="16" cy="16" r="15.5" stroke="#0A5042"/><path d="M16 7l7 6.5V25h-4.6v-6.2h-4.8V25H9V13.5L16 7z" fill="#fff"/><circle cx="16" cy="13.6" r="1.7" fill="#E07B39"/></svg>
|
| 102 |
</div>
|
| 103 |
+
<h1 class="intake-h1">Fugee</h1>
|
| 104 |
<p class="intake-tagline">Safe guidance for people on the move</p>
|
| 105 |
<p class="intake-lbl">Choose your language</p>
|
| 106 |
</div>
|
app/phases/interview.py
CHANGED
|
@@ -17,6 +17,7 @@ import gradio as gr
|
|
| 17 |
|
| 18 |
from agent.events import TextDeltaEvent
|
| 19 |
from agent.loop import create_loop
|
|
|
|
| 20 |
from app.countries import country_choices, country_name
|
| 21 |
from app.prompt_loader import load_prompt
|
| 22 |
from app.interview_script import (
|
|
@@ -102,13 +103,8 @@ def render_rail(state: State) -> str:
|
|
| 102 |
return f'<div class="iv-rail" aria-label="Interview progress">{"".join(pills)}</div>'
|
| 103 |
|
| 104 |
|
| 105 |
-
def _bold(escaped: str) -> str:
|
| 106 |
-
"""Render **markdown bold** (used for review summary labels)."""
|
| 107 |
-
return re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", escaped)
|
| 108 |
-
|
| 109 |
-
|
| 110 |
def _bubble(role: str, text: str, current: bool = False) -> str:
|
| 111 |
-
safe =
|
| 112 |
if role == "user":
|
| 113 |
return ('<div class="iv-msg iv-msg--user"><div class="iv-msg__av"><span>You</span></div>'
|
| 114 |
f'<div class="iv-msg__bubble">{safe}</div></div>')
|
|
|
|
| 17 |
|
| 18 |
from agent.events import TextDeltaEvent
|
| 19 |
from agent.loop import create_loop
|
| 20 |
+
from app import mdlite
|
| 21 |
from app.countries import country_choices, country_name
|
| 22 |
from app.prompt_loader import load_prompt
|
| 23 |
from app.interview_script import (
|
|
|
|
| 103 |
return f'<div class="iv-rail" aria-label="Interview progress">{"".join(pills)}</div>'
|
| 104 |
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
def _bubble(role: str, text: str, current: bool = False) -> str:
|
| 107 |
+
safe = mdlite.inline(text).replace("\n", "<br>")
|
| 108 |
if role == "user":
|
| 109 |
return ('<div class="iv-msg iv-msg--user"><div class="iv-msg__av"><span>You</span></div>'
|
| 110 |
f'<div class="iv-msg__bubble">{safe}</div></div>')
|
app/phases/recommendations.py
CHANGED
|
@@ -133,7 +133,7 @@ def card_body_html(rec: dict) -> str:
|
|
| 133 |
)
|
| 134 |
docs = rec.get("requiredDocuments") or []
|
| 135 |
prep_items = "".join(f"<li>{html.escape(str(d))}</li>" for d in docs) or \
|
| 136 |
-
"<li>
|
| 137 |
return (
|
| 138 |
'<div class="ccard__top">'
|
| 139 |
f'<span class="ccard__flag">{flag}</span>'
|
|
@@ -170,7 +170,7 @@ def roadmap_html(rec: dict | None) -> str:
|
|
| 170 |
"protects you from being returned.",
|
| 171 |
unhcr_contact, "1–4 weeks for appointment"),
|
| 172 |
("File your asylum claim",
|
| 173 |
-
"Submit your personal statement and supporting evidence.
|
| 174 |
"already drafted these for you.",
|
| 175 |
"Asylum authority / RSD unit", "Same week as registration"),
|
| 176 |
("Refugee status interview (RSD)",
|
|
|
|
| 133 |
)
|
| 134 |
docs = rec.get("requiredDocuments") or []
|
| 135 |
prep_items = "".join(f"<li>{html.escape(str(d))}</li>" for d in docs) or \
|
| 136 |
+
"<li>Fugee will help you gather what you need.</li>"
|
| 137 |
return (
|
| 138 |
'<div class="ccard__top">'
|
| 139 |
f'<span class="ccard__flag">{flag}</span>'
|
|
|
|
| 170 |
"protects you from being returned.",
|
| 171 |
unhcr_contact, "1–4 weeks for appointment"),
|
| 172 |
("File your asylum claim",
|
| 173 |
+
"Submit your personal statement and supporting evidence. Fugee has "
|
| 174 |
"already drafted these for you.",
|
| 175 |
"Asylum authority / RSD unit", "Same week as registration"),
|
| 176 |
("Refugee status interview (RSD)",
|
app/prompts/system.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
You are **
|
| 2 |
who have been forced to leave their homes — displaced people, asylum seekers,
|
| 3 |
and refugees. You behave like a trusted, experienced case worker: patient,
|
| 4 |
clear, never rushed, never alarmist. Your purpose is to lower fear and give the
|
|
@@ -6,7 +6,7 @@ person back a sense of control over their situation.
|
|
| 6 |
|
| 7 |
## Who you are talking to
|
| 8 |
|
| 9 |
-
The person using
|
| 10 |
affect their safety and their family's future. They may be reading in a second
|
| 11 |
or third language, on a phone, under stress. Treat every message as if it comes
|
| 12 |
from someone who deserves dignity, patience, and honesty.
|
|
|
|
| 1 |
+
You are **Fugee**, a calm, knowledgeable, and compassionate guide for people
|
| 2 |
who have been forced to leave their homes — displaced people, asylum seekers,
|
| 3 |
and refugees. You behave like a trusted, experienced case worker: patient,
|
| 4 |
clear, never rushed, never alarmist. Your purpose is to lower fear and give the
|
|
|
|
| 6 |
|
| 7 |
## Who you are talking to
|
| 8 |
|
| 9 |
+
The person using Fugee may be frightened, exhausted, and making decisions that
|
| 10 |
affect their safety and their family's future. They may be reading in a second
|
| 11 |
or third language, on a phone, under stress. Treat every message as if it comes
|
| 12 |
from someone who deserves dignity, patience, and honesty.
|