File size: 7,396 Bytes
bda6294
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# -*- coding: utf-8 -*-
"""
Assistant Vizyon Ayiti 360 - interface epuree pour Hugging Face Space.
Chat + televersement de documents. RAG fonde uniquement sur le contenu du
site (public + non public selon les droits). FR / EN / Kreyol.

Lancer / run:  streamlit run app.py
"""
import streamlit as st

import config
import gdpr
import ingest

st.set_page_config(page_title=config.APP_TITLE,
                   page_icon=config.FAVICON_URL or "馃寠", layout="centered")

# --------------------------------------------------------------------------
# Traductions / translations
# --------------------------------------------------------------------------
T = {
    "fr": {
        "subtitle": "Posez vos questions. Reponses fondees sur le site Vizyon Ayiti 360.",
        "ask": "Votre question...",
        "sources": "Sources",
        "upload": "Joindre un document (PDF, DOCX, TXT, CSV)",
        "doc_hint": "Un document est joint : votre prochaine question portera dessus.",
        "clear": "Effacer la conversation",
        "thinking": "Recherche...",
        "building": "Indexation du contenu du site (premiere fois)...",
        "internal": "interne",
        "index_err": "Impossible de construire l'index du site.",
    },
    "en": {
        "subtitle": "Ask your questions. Answers grounded in the Vizyon Ayiti 360 site.",
        "ask": "Your question...",
        "sources": "Sources",
        "upload": "Attach a document (PDF, DOCX, TXT, CSV)",
        "doc_hint": "A document is attached: your next question will use it.",
        "clear": "Clear conversation",
        "thinking": "Searching...",
        "building": "Indexing the site content (first run)...",
        "internal": "internal",
        "index_err": "Could not build the site index.",
    },
    "ht": {
        "subtitle": "Poze kesyon ou yo. Repons yo baze sou sit Vizyon Ayiti 360.",
        "ask": "Kesyon ou...",
        "sources": "Sous",
        "upload": "Mete yon dokiman (PDF, DOCX, TXT, CSV)",
        "doc_hint": "Yon dokiman mete la : pwochen kesyon ou an ap sou li.",
        "clear": "Efase konv猫sasyon an",
        "thinking": "Y ap ch猫che...",
        "building": "Endeksasyon kontni sit la (premye fwa)...",
        "internal": "enten",
        "index_err": "Pa kapab konstwi end猫ks sit la.",
    },
}
_LANG_LABELS = {"Francais": "fr", "English": "en", "Kreyol": "ht"}
_LANG_ORDER = ["Francais", "English", "Kreyol"]

if "lang" not in st.session_state:
    st.session_state.lang = config.DEFAULT_LANG
if "messages" not in st.session_state:
    st.session_state.messages = []


def t(key):
    return T[st.session_state.lang][key]


# --------------------------------------------------------------------------
# Portillon d'acces optionnel (jeton partage passe dans l'URL de l'iframe)
# Optional access gate (shared token passed in the iframe URL)
# --------------------------------------------------------------------------
def _check_access():
    if not config.APP_ACCESS_TOKEN:
        return  # portillon desactive
    try:
        provided = st.query_params.get("access", "")
    except Exception:
        provided = st.experimental_get_query_params().get("access", [""])[0]
    if provided != config.APP_ACCESS_TOKEN:
        st.error("Acces restreint. / Restricted access.")
        st.stop()


_check_access()


# --------------------------------------------------------------------------
# Indexation automatique (une fois par conteneur) / auto-index once per container
# --------------------------------------------------------------------------
@st.cache_resource(show_spinner=False)
def ensure_index():
    if config.REBUILD_ON_START or not ingest.index_exists():
        ingest.build_index()
    return ingest.load_meta()


# --------------------------------------------------------------------------
# Barre laterale / sidebar
# --------------------------------------------------------------------------
with st.sidebar:
    if config.LOGO_URL:
        st.image(config.LOGO_URL, use_container_width=True)
    st.markdown("### Langue / Language / Lang")
    _cur = {"fr": 0, "en": 1, "ht": 2}.get(st.session_state.lang, 0)
    choice = st.radio("lang", _LANG_ORDER, index=_cur,
                      label_visibility="collapsed")
    st.session_state.lang = _LANG_LABELS[choice]

    st.markdown("---")
    up = st.file_uploader(t("upload"), type=["pdf", "docx", "txt", "md", "csv"])
    if up:
        st.caption(t("doc_hint"))

    st.markdown("---")
    st.info(gdpr.notice(st.session_state.lang))

    meta = ingest.load_meta()
    if meta:
        extra = ""
        if meta.get("non_public"):
            extra = f" 路 {meta['non_public']} {t('internal')}"
        st.caption(f"{meta['chunks']} passages{extra}\n\n{meta['built_at']}")

# --------------------------------------------------------------------------
# En-tete / header
# --------------------------------------------------------------------------
st.caption(t("subtitle"))

# Construction de l'index (avec retour visuel au premier lancement).
try:
    with st.spinner(t("building")):
        ensure_index()
except Exception as e:
    st.error(f"{t('index_err')} ({e})")
    st.stop()

# --------------------------------------------------------------------------
# Historique / history
# --------------------------------------------------------------------------
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])
        if msg.get("sources"):
            with st.expander(t("sources")):
                for s in msg["sources"]:
                    tag = "" if s.get("status", "publish") == "publish" \
                        else f" _({t('internal')})_"
                    st.markdown(f"- [{s['title']}]({s['url']}){tag}  路  "
                                f"score {s['score']:.2f}")

# --------------------------------------------------------------------------
# Saisie / input
# --------------------------------------------------------------------------
prompt = st.chat_input(t("ask"))
if prompt:
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)
    with st.chat_message("assistant"):
        with st.spinner(t("thinking")):
            try:
                if up is not None:
                    import document_analyzer
                    resp, sources, _ = document_analyzer.analyze_document(
                        up.name, up.getvalue(), prompt, lang=st.session_state.lang)
                else:
                    import rag_engine
                    resp, sources = rag_engine.answer(
                        prompt, lang=st.session_state.lang)
            except Exception as e:
                resp, sources = f"Erreur : {e}", []
        st.markdown(resp)
        if sources:
            with st.expander(t("sources")):
                for s in sources:
                    tag = "" if s.get("status", "publish") == "publish" \
                        else f" _({t('internal')})_"
                    st.markdown(f"- [{s['title']}]({s['url']}){tag}  路  "
                                f"score {s['score']:.2f}")
    st.session_state.messages.append(
        {"role": "assistant", "content": resp, "sources": sources})

if st.session_state.messages:
    if st.button(t("clear")):
        st.session_state.messages = []
        st.rerun()