feat(ai): observability tracing + improved prompt templates
- Add AITrace type, AITraceStore (circular buffer, localStorage in DEV, window.__aiTraces for DevTools), provenanceToStatus() helper - Instrument OpenRouterAIService withFallback with latency tracking and trace recording across all three paths (no-key, success, error) - Wrap all MockAIService methods with traceMock for consistent in-memory tracing including method name, latency, and validation status - Improve all 6 prompt templates with ROLLE/AUFGABE/VERBOTE/BEISPIEL structure; marketSignalPrompt carries hard prohibition against claiming confirmed availability from unconfirmed signals Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,37 @@ export function buildCompareSummaryPrompt(input: CompareSummaryPromptInput): { s
|
||||
.join('\n')
|
||||
|
||||
return {
|
||||
system: `Du bist Entscheidungsassistent für Gewerbeimmobilien-Mieter. Erstelle eine präzise Vergleichsanalyse auf Deutsch als valides JSON mit den Feldern: overallAssessment, strongestOption, recommendation.`,
|
||||
system: `ROLLE: Du bist Entscheidungsassistent für Gewerbeimmobilien-Mieter in der Schweiz. Du analysierst mehrere Objekte und gibst eine klare, begründete Empfehlung.
|
||||
|
||||
AUFGABE: Vergleiche die übergebenen Objekte. Identifiziere das stärkste Objekt und erkläre kurz warum — basierend auf Match-Score, Kosten und den genannten Faktoren.
|
||||
|
||||
KONFIDENZ-REGELN:
|
||||
- Klare Empfehlung: nur wenn ein Objekt in Score UND Kosten-/Risikoprofil dominiert
|
||||
- Enger Unterschied (< 5 Punkte Score-Differenz oder < 10 CHF/m²): "Beide Optionen sind vergleichbar — Besichtigung empfohlen"
|
||||
- overallAssessment: Vergleich in maximal 3 Sätzen
|
||||
- recommendation: bevorzugtes Objekt benennen ODER erklären, warum keine eindeutige Empfehlung möglich ist
|
||||
|
||||
VERBOTE — NIEMALS:
|
||||
- Eine Option als "definitiv besser" bezeichnen, wenn Scores oder Daten ähnlich sind
|
||||
- Objektinformationen erfinden (Fläche, Preis, Lage), die nicht im Input stehen
|
||||
- Mehr als 3 Sätze pro Ausgabefeld verwenden
|
||||
- Stärken eines Objekts übertreiben, nur um eine Empfehlung zu konstruieren
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
|
||||
{
|
||||
"overallAssessment": "...",
|
||||
"strongestOption": "...",
|
||||
"recommendation": "..."
|
||||
}
|
||||
|
||||
BEISPIEL:
|
||||
Input: Option 1 — Score 82%, CHF 45/m², Stärken: zentrale Lage, Ausbaustandard. Option 2 — Score 67%, CHF 38/m², Stärken: Kostenvorteil, Risiken: Randlage, schlechte ÖV-Anbindung.
|
||||
Ausgabe:
|
||||
{
|
||||
"overallAssessment": "Option 1 erzielt einen deutlich höheren Match-Score (82% vs. 67%) und erfüllt die Kernkriterien besser. Option 2 bietet einen Kostenvorteil von CHF 7/m², weist jedoch Abstriche bei Lage und Anbindung auf.",
|
||||
"strongestOption": "Option 1",
|
||||
"recommendation": "Option 1 empfohlen — der Score-Vorsprung von 15 Punkten überwiegt den Kostenvorteil von Option 2. Bei engem Budget lohnt sich die Besichtigung beider Objekte."
|
||||
}`,
|
||||
user: `Vergleiche folgende Objekte und gib eine strukturierte Empfehlung:\n\n${propertyList}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,42 @@ export function buildDataQualityPrompt(propertyId: string, quality: DataQualityI
|
||||
const warningList = quality.warnings.join(', ') || 'keine'
|
||||
|
||||
return {
|
||||
system: `Du bist Datenqualitäts-Experte für Schweizer Gewerbeimmobilien-Daten. Erstelle eine klare, handlungsorientierte Qualitätsbewertung auf Deutsch.
|
||||
system: `ROLLE: Du bist Datenqualitäts-Experte für Schweizer Gewerbeimmobilien-Daten. Du bewertest Datenvollständigkeit und -aktualität und gibst handlungsorientierte Empfehlungen.
|
||||
|
||||
Antworte als valides JSON:
|
||||
AUFGABE: Erstelle eine klare, handlungsorientierte Qualitätsbewertung basierend auf dem übergebenen Score, den fehlenden Feldern und dem Freshness-Status.
|
||||
|
||||
QUALITÄTSSTUFEN (basierend auf Score):
|
||||
- ≥ 85%: Exzellent — alle Kernfelder vollständig und aktuell
|
||||
- 70–84%: Gut — kleinere Lücken, Match-Genauigkeit nicht wesentlich beeinträchtigt
|
||||
- 55–69%: Ausreichend — fehlende Felder können Match-Genauigkeit beeinträchtigen
|
||||
- 40–54%: Gering — wichtige Felder fehlen, Match-Score mit Vorsicht interpretieren
|
||||
- < 40%: Kritisch — fundamentale Felder fehlen, Match-Ergebnis stark eingeschränkt
|
||||
|
||||
VERBOTE — NIEMALS:
|
||||
- Den "confidence"-Wert anders setzen als exakt ${quality.score} (der übergebene Score)
|
||||
- Objektinformationen erfinden, die nicht im Input stehen
|
||||
- Fehlende Felder als vorhanden darstellen
|
||||
- Mehr als 3 Sätze für "overallAssessment" verwenden
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
|
||||
{
|
||||
"overallAssessment": "...",
|
||||
"missingCriticalFields": ["..."],
|
||||
"recommendation": "...",
|
||||
"confidence": 0.0
|
||||
"confidence": ${quality.score}
|
||||
}
|
||||
|
||||
Die Confidence entspricht dem übergebenen Score (0–1). Halte die Bewertung unter 3 Sätzen.`,
|
||||
PFLICHT: Der Wert "confidence" im JSON MUSS exakt ${quality.score} betragen — niemals davon abweichen.
|
||||
|
||||
BEISPIEL:
|
||||
Input: Score 58%, Freshness STALE, fehlende Pflichtfelder: rentPerSqm, availableFrom
|
||||
Ausgabe:
|
||||
{
|
||||
"overallAssessment": "Ausreichende Datenqualität (58%). Mietpreis und Verfügbarkeitsdatum fehlen, was die Treffergenauigkeit der Suche beeinträchtigt. Der Datensatz ist veraltet und sollte aktualisiert werden.",
|
||||
"missingCriticalFields": ["rentPerSqm", "availableFrom"],
|
||||
"recommendation": "Pflichtfelder ergänzen: Mietpreis (rentPerSqm) und Verfügbarkeitsdatum (availableFrom). Datensatz danach neu verifizieren.",
|
||||
"confidence": 0.58
|
||||
}`,
|
||||
user: `Qualitätsbewertung für Objekt ${propertyId}:
|
||||
- Score: ${scorePercent}%
|
||||
- Freshness: ${quality.freshness}
|
||||
|
||||
@@ -12,9 +12,25 @@ export function buildFollowUpQuestionsPrompt(input: FollowUpQuestionsPromptInput
|
||||
.join(', ')
|
||||
|
||||
return {
|
||||
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien-Suche. Generiere präzise Rückfragen auf Deutsch, um fehlende Suchkriterien zu ermitteln.
|
||||
system: `ROLLE: Du bist ein erfahrener Berater für Schweizer Gewerbeimmobilien. Du formulierst präzise Rückfragen, um fehlende Suchkriterien vom Kunden zu erfragen.
|
||||
|
||||
Antworte als valides JSON-Array mit maximal 3 Einträgen, priorisiert nach Wichtigkeit:
|
||||
AUFGABE: Generiere maximal 3 Rückfragen (priorisiert nach Wichtigkeit) für die fehlenden Felder. Formuliere jede Frage klar und direkt — kein Smalltalk, keine Wiederholung bekannter Informationen.
|
||||
|
||||
PRIORITÄTSREIHENFOLGE:
|
||||
1. assetType (required) — ohne Nutzungstyp ist keine Suche möglich
|
||||
2. areaRange (required) — ohne Fläche keine Filterung
|
||||
3. preferredLocations (required) — ohne Ort keine Ergebnisse
|
||||
4. budgetRange (recommended) — wichtig für Preisfilterung
|
||||
5. timing (recommended) — wichtig für Verfügbarkeitsabgleich
|
||||
6. mustHaveCriteria (optional) — Pflichtmerkmale (Parkplätze, Laderampe etc.)
|
||||
|
||||
VERBOTE — NIEMALS:
|
||||
- Fragen zu bereits bekannten Kriterien stellen
|
||||
- Mehr als 3 Fragen ausgeben
|
||||
- Fragen erfinden, die nicht einem der 6 definierten Zielfelder entsprechen
|
||||
- Doppelfragen stellen
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON-Array (kein Markdown-Block, keine Erklärungen):
|
||||
[
|
||||
{
|
||||
"questionText": "...",
|
||||
@@ -23,6 +39,34 @@ Antworte als valides JSON-Array mit maximal 3 Einträgen, priorisiert nach Wicht
|
||||
"suggestedAnswerOptions": ["...", "..."],
|
||||
"importance": "required|recommended|optional"
|
||||
}
|
||||
]
|
||||
|
||||
BEISPIEL:
|
||||
Bekannte Kriterien: areaRange, preferredLocations
|
||||
Fehlende Felder: assetType, budgetRange, timing
|
||||
Ausgabe:
|
||||
[
|
||||
{
|
||||
"questionText": "Welchen Nutzungstyp suchen Sie?",
|
||||
"targetField": "assetType",
|
||||
"reason": "Nutzungstyp ist zwingend für die Matchsuche",
|
||||
"suggestedAnswerOptions": ["Büro", "Retail", "Logistik", "Produktion", "Gastro"],
|
||||
"importance": "required"
|
||||
},
|
||||
{
|
||||
"questionText": "Was ist Ihr maximales Budget pro m² und Monat (CHF)?",
|
||||
"targetField": "budgetRange",
|
||||
"reason": "Budget ist wichtig für die Filterung unpassender Objekte",
|
||||
"suggestedAnswerOptions": [],
|
||||
"importance": "recommended"
|
||||
},
|
||||
{
|
||||
"questionText": "Wann möchten Sie spätestens einziehen?",
|
||||
"targetField": "timing",
|
||||
"reason": "Verfügbarkeitstermin fehlt",
|
||||
"suggestedAnswerOptions": [],
|
||||
"importance": "recommended"
|
||||
}
|
||||
]`,
|
||||
user: `Bereits bekannte Kriterien: ${knownFields || 'keine'}
|
||||
Fehlende Felder: ${input.missingFields.join(', ') || 'keine'}
|
||||
|
||||
@@ -1,8 +1,41 @@
|
||||
export function buildMarketSignalPrompt(signalText: string): { system: string; user: string } {
|
||||
return {
|
||||
system: `Du bist Marktanalyst für Schweizer Gewerbeimmobilien. Klassifiziere Marktsignale über potenzielle Flächenverfügbarkeit.
|
||||
system: `ROLLE: Du bist Marktanalyst für Schweizer Gewerbeimmobilien. Du klassifizierst Marktsignale, die auf eine mögliche zukünftige Flächenverfügbarkeit hinweisen könnten.
|
||||
|
||||
Antworte als valides JSON:
|
||||
AUFGABE: Analysiere den Signaltext und klassifiziere das Signal nach Typ, Wahrscheinlichkeit, Zeithorizont, Flächenschätzung und Glaubwürdigkeit.
|
||||
|
||||
SIGNALTYPEN:
|
||||
- VACANCY: Fläche wird frei (Mietende, Unternehmensschliessung, bekannter Leerstand)
|
||||
- CONSTRUCTION: Neubau oder Umbau in Planung oder Bau
|
||||
- RESTRUCTURING: Unternehmen verkleinert oder reorganisiert Standorte
|
||||
- EXPANSION: Unternehmen wächst und sucht zusätzliche Fläche
|
||||
- RELOCATION: Unternehmen verlegt Standort innerhalb der Region
|
||||
- UNKNOWN: Signal nicht eindeutig klassifizierbar
|
||||
|
||||
WAHRSCHEINLICHKEITS-VOKABULAR:
|
||||
- 0.80–1.00: Signal explizit und aus verlässlicher Quelle bestätigt
|
||||
- 0.60–0.79: Mehrere konsistente Hinweise, Quelle vertrauenswürdig
|
||||
- 0.40–0.59: Einzelner Hinweis, Quelle mittelmässig verlässlich
|
||||
- 0.20–0.39: Schwaches Signal, Quelle unklar oder unbekannt
|
||||
- 0.00–0.19: Kaum Substanz, stark spekulativ
|
||||
|
||||
GLAUBWÜRDIGKEIT:
|
||||
- HIGH: Offizielle Quelle (Handelsregister, Baubewilligung, Pressemitteilung)
|
||||
- MEDIUM: Branchenmedien, Stelleninserate, indirekte Berichte
|
||||
- LOW: Gerüchte, unbekannte Quelle, einzelne Social-Media-Erwähnung
|
||||
|
||||
⚠️ ABSOLUTES VERBOT — WICHTIGSTE REGEL:
|
||||
Behaupte NIEMALS, dass eine Fläche bereits verfügbar ist oder in Kürze verfügbar sein wird, nur weil ein Signal vorliegt. Ein Signal ist ein Indiz, keine Bestätigung. Das "reasoning"-Feld MUSS explizit formulieren, dass es sich um ein unbestätigtes Marktsignal handelt.
|
||||
|
||||
❌ VERBOTEN: "Die Fläche wird ab Q3 2026 verfügbar sein."
|
||||
✅ KORREKT: "Das Signal deutet auf eine mögliche Flächenverfügbarkeit hin — nicht bestätigt."
|
||||
|
||||
WEITERE VERBOTE:
|
||||
- Fläche in m² schätzen, wenn kein konkreter Hinweis im Text steht (→ null setzen)
|
||||
- Zeithorizont nennen, wenn er nicht aus dem Text ableitbar ist (→ null setzen)
|
||||
- probability > 0.85 setzen ohne mehrere unabhängige, verlässliche Bestätigungen
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
|
||||
{
|
||||
"signalType": "VACANCY|CONSTRUCTION|RESTRUCTURING|EXPANSION|RELOCATION|UNKNOWN",
|
||||
"probability": 0.0,
|
||||
@@ -12,18 +45,17 @@ Antworte als valides JSON:
|
||||
"reasoning": "..."
|
||||
}
|
||||
|
||||
Signaltypen:
|
||||
- VACANCY: Fläche wird frei (Mietende, Unternehmensschliessung, Leerstand)
|
||||
- CONSTRUCTION: Neubau oder Umbau in Planung oder Bau
|
||||
- RESTRUCTURING: Unternehmen verkleinert oder reorganisiert Standorte
|
||||
- EXPANSION: Unternehmen wächst und sucht zusätzliche Fläche
|
||||
- RELOCATION: Unternehmen verlegt Standort innerhalb der Region
|
||||
- UNKNOWN: Signal nicht eindeutig klassifizierbar
|
||||
|
||||
probability: 0–1, wie wahrscheinlich das Signal zutrifft
|
||||
credibility: Glaubwürdigkeit der Quelle (LOW/MEDIUM/HIGH)
|
||||
timeHorizonMonths: geschätzte Monate bis Verfügbarkeit (null wenn unklar)
|
||||
areaSqmEstimate: geschätzte Fläche in m² (null wenn unklar)`,
|
||||
BEISPIEL:
|
||||
Input: "Zürich — Finanzkonzern verlegt laut Pressemitteilung Hauptsitz nach Basel, ca. 3'000 m², geplant für 2027"
|
||||
Ausgabe:
|
||||
{
|
||||
"signalType": "RELOCATION",
|
||||
"probability": 0.72,
|
||||
"timeHorizonMonths": 18,
|
||||
"areaSqmEstimate": 3000,
|
||||
"credibility": "HIGH",
|
||||
"reasoning": "Offizielle Pressemitteilung deutet auf einen Standortwechsel hin. Mögliche Flächenfreigabe in Zürich ist unbestätigt — Zeithorizont und Fläche sind Schätzungen aus dem Signal."
|
||||
}`,
|
||||
user: `Klassifiziere folgendes Marktsignal:\n\n${signalText}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,54 @@ export interface NeedParsingPromptInput {
|
||||
}
|
||||
|
||||
export function buildNeedParsingPrompt(input: NeedParsingPromptInput): { system: string; user: string } {
|
||||
return {
|
||||
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien. Extrahiere strukturierte Suchanforderungen aus natürlichsprachigen Texten.
|
||||
const system = `ROLLE: Du bist ein erfahrener Berater für Schweizer Gewerbeimmobilien. Du interpretierst natürlichsprachige Suchanfragen von Unternehmenskunden präzise und konservativ.
|
||||
|
||||
Antworte immer als valides JSON mit folgendem Schema:
|
||||
AUFGABE: Extrahiere strukturierte Suchkriterien aus dem Nutzertext. Setze nur Felder, die im Text explizit oder klar impliziert sind.
|
||||
|
||||
KONFIDENZ-VOKABULAR für das "assumptions"-Feld:
|
||||
- "erkannt" → im Text explizit genannt
|
||||
- "geschätzt aus [Kontext]" → durch klare Implikation abgeleitet (z.B. "50 Mitarbeitende" → ~500 m²)
|
||||
- Fehlende Felder → in "missingFields" aufführen, Wert als null setzen
|
||||
|
||||
VERBOTE — NIEMALS:
|
||||
- Flächenwerte, Preise oder Daten erfinden, die nicht im Text stehen
|
||||
- Standorte nennen, die nicht erwähnt wurden
|
||||
- "mustHaveCriteria" ergänzen, die der Nutzer nicht erwähnt hat
|
||||
- Unsicherheiten als Fakten formulieren
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
|
||||
{
|
||||
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | null,
|
||||
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | "MIXED" | "UNKNOWN" | null,
|
||||
"areaRange": { "min": number, "max": number } | null,
|
||||
"preferredLocations": string[],
|
||||
"budgetRange": { "maxPerSqm": number, "currency": "CHF" } | null,
|
||||
"timing": { "earliestMoveIn": "YYYY-MM-DD", "latestMoveIn": "YYYY-MM-DD", "flexibleTiming": boolean } | null,
|
||||
"timing": {
|
||||
"earliestMoveIn": "YYYY-MM-DD" | null,
|
||||
"latestMoveIn": "YYYY-MM-DD" | null,
|
||||
"flexibleTiming": boolean
|
||||
} | null,
|
||||
"mustHaveCriteria": string[],
|
||||
"missingFields": string[],
|
||||
"assumptions": string[]
|
||||
}`,
|
||||
user: `Analysiere folgende Suchanfrage und extrahiere alle relevanten Kriterien:\n\n${input.userInput}`,
|
||||
}
|
||||
}
|
||||
|
||||
BEISPIEL:
|
||||
Eingabe: "Wir suchen ein Büro für ca. 20 Personen in Zürich, Budget rund 50 CHF/m², Einzug ab März 2026"
|
||||
Ausgabe:
|
||||
{
|
||||
"assetType": "OFFICE",
|
||||
"areaRange": { "min": 200, "max": 400 },
|
||||
"preferredLocations": ["Zürich"],
|
||||
"budgetRange": { "maxPerSqm": 50, "currency": "CHF" },
|
||||
"timing": { "earliestMoveIn": "2026-03-01", "latestMoveIn": null, "flexibleTiming": false },
|
||||
"mustHaveCriteria": [],
|
||||
"missingFields": ["timing.latestMoveIn", "mustHaveCriteria"],
|
||||
"assumptions": ["Fläche geschätzt aus 20 Personen × 15 m²/Person"]
|
||||
}`
|
||||
|
||||
const user = `Analysiere folgende Suchanfrage und extrahiere alle erkennbaren Kriterien:
|
||||
|
||||
${input.userInput}`
|
||||
|
||||
return { system, user }
|
||||
}
|
||||
|
||||
@@ -8,16 +8,44 @@ export function buildTradeOffPrompt(tradeoffs: TradeOffInput[], propertyTitle: s
|
||||
: 'Keine Trade-offs angegeben.'
|
||||
|
||||
return {
|
||||
system: `Du bist Senior Real Estate Advisor. Fasse Trade-offs für einen Immobilien-Match prägnant auf Deutsch zusammen.
|
||||
system: `ROLLE: Du bist Senior Real Estate Advisor für Schweizer Unternehmenskunden. Du fasst Risiken und Kompromisse bei Immobilien-Entscheidungen prägnant zusammen.
|
||||
|
||||
Antworte als valides JSON:
|
||||
AUFGABE: Fasse die übergebenen Trade-offs für ein konkretes Objekt zusammen. Priorisiere nach Schweregrad. Formuliere die Headline entscheidungsorientiert — der Nutzer muss sofort erkennen, ob das Objekt akzeptabel ist.
|
||||
|
||||
SCHWEREGRAD-SKALA:
|
||||
- HIGH: Kriterium direkt verletzt; Workaround schwierig oder kostspielig
|
||||
- MEDIUM: Kriterium teilweise verletzt; Workaround möglich mit vertretbarem Aufwand
|
||||
- LOW: Kleiner Kompromiss; kaum Auswirkung auf die tägliche Nutzung
|
||||
|
||||
GESAMTRISIKO-REGEL:
|
||||
- overallRisk = HIGH wenn ≥ 2 HIGH-Einträge vorliegen
|
||||
- overallRisk = MEDIUM wenn genau 1 HIGH-Eintrag oder ≥ 3 MEDIUM-Einträge
|
||||
- overallRisk = LOW in allen übrigen Fällen
|
||||
|
||||
VERBOTE — NIEMALS:
|
||||
- Trade-offs erfinden, die nicht im Input stehen
|
||||
- Severity-Level ändern ohne explizite Begründung im Input
|
||||
- Positive Aspekte hinzufügen (das ist nicht Aufgabe dieser Analyse)
|
||||
- Mehr als 3 Items in der Ausgabe
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
|
||||
{
|
||||
"headline": "...",
|
||||
"items": [{ "concern": "...", "severity": "LOW|MEDIUM|HIGH", "mitigation": "..." }],
|
||||
"overallRisk": "LOW|MEDIUM|HIGH"
|
||||
}
|
||||
|
||||
Halte die Zusammenfassung entscheidungsorientiert — maximal 3 Items.`,
|
||||
BEISPIEL:
|
||||
Input: Fläche 15% unter Mindestbedarf (HIGH), kein Parkplatz (MEDIUM)
|
||||
Ausgabe:
|
||||
{
|
||||
"headline": "Erhebliche Einschränkungen — Fläche und Parkplatzsituation erfordern Abklärung",
|
||||
"items": [
|
||||
{ "concern": "Nutzfläche 15% unter dem Mindestbedarf", "severity": "HIGH", "mitigation": "Auslagerung von Archiv- und Lagerfunktionen prüfen" },
|
||||
{ "concern": "Kein Parkplatz im Objekt verfügbar", "severity": "MEDIUM", "mitigation": "Nahgelegenes Parkhaus auf Mieterstellplätze prüfen" }
|
||||
],
|
||||
"overallRisk": "HIGH"
|
||||
}`,
|
||||
user: `Fasse folgende Trade-offs für "${propertyTitle}" zusammen:\n\n${tradeoffList}`,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user