File size: 15,896 Bytes
875b439
b931367
 
 
 
1b21038
b931367
 
875b439
b931367
c383152
b931367
1b21038
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c383152
 
1b21038
 
c383152
1b21038
c383152
 
1b21038
 
 
 
c383152
1b21038
c383152
 
 
b931367
 
c383152
b931367
 
 
c383152
439ab17
 
 
1b21038
b931367
 
c383152
b931367
c383152
1b21038
 
 
 
b931367
 
1b21038
b931367
c383152
 
1b21038
 
b931367
1b21038
 
 
 
 
 
 
 
 
 
 
 
c383152
1b21038
b931367
 
 
c383152
b931367
1b21038
c383152
b931367
 
 
1b21038
b931367
 
 
c383152
b931367
c383152
 
b931367
c383152
b931367
 
875b439
c383152
 
b931367
 
 
 
 
1b21038
 
 
b931367
 
c383152
 
b931367
 
c383152
b931367
875b439
 
 
b931367
c383152
b931367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b21038
b931367
 
 
c383152
1b21038
 
 
 
 
 
 
 
 
 
 
b931367
 
 
 
 
1b21038
c383152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b21038
b931367
 
 
 
 
 
 
1b21038
b931367
 
 
 
 
 
 
 
1b21038
b931367
 
 
c383152
1b21038
c383152
 
 
 
 
 
 
 
 
 
 
 
 
 
875b439
 
 
 
 
c383152
 
 
 
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
from mimetypes import init
import gradio as gr
import uuid
from datetime import datetime
import pandas as pd
import re
from model_handler import ModelHandler
from config import CHAT_MODEL_SPECS, LING_1T
from recommand_config import get_recommended_inputs
from ui_components.model_selector import create_model_selector
from i18n import get_text

def on_app_load(request: gr.Request, history, conv_id, current_lang_state):
    """
    Handles the application's initial state on load.
    - Determines language from URL parameter.
    - Loads conversation history or creates a new one.
    """
    # --- Language Detection ---
    query_params = dict(request.query_params)
    url_lang = query_params.get("lang")

    updated_lang = current_lang_state  # Start with the default
    if url_lang and url_lang in ["en", "zh"]:
        updated_lang = url_lang

    # --- History Loading Logic ---
    if not history:
        # First time ever, create a new conversation
        conv_id = str(uuid.uuid4())
        new_convo_title = get_text("chat_new_conversation_title", updated_lang)
        new_convo = {
            "id": conv_id,
            "title": new_convo_title,
            "messages": [],
            "timestamp": datetime.now().isoformat(),
            "system_prompt": "",
            "model": CHAT_MODEL_SPECS[LING_1T]["display_name"],
            "temperature": 0.7
        }
        history = [new_convo]
        return (
            conv_id,
            history,
            gr.update(value=get_history_df(history, updated_lang)),
            [],
            updated_lang,
        )

    if conv_id and any(c["id"] == conv_id for c in history):
        # Valid last session, load it
        for convo in history:
            if convo["id"] == conv_id:
                return (
                    conv_id,
                    history,
                    gr.update(value=get_history_df(history, updated_lang)),
                    convo["messages"],
                    updated_lang,
                )

    # Fallback to most recent conversation
    most_recent_convo = history[0]
    return (
        most_recent_convo["id"],
        history,
        gr.update(value=get_history_df(history, updated_lang)),
        most_recent_convo["messages"],
        updated_lang,
    )


def generate_conversation_title(messages, system_prompt):
    """
    Generates a conversation title based on a heuristic, defensively handling
    multiple possible message formats.
    1. Tries to use the first user query.
    2. Falls back to the system prompt.
    3. Falls back to the current time.
    """
    first_query = None
    
    # Rule 1: Try to extract the first user query from various possible formats
    if messages:
        first_message = messages[0]
        # Case 1: List[List[str]] -> [['user', 'assistant'], ...]
        if isinstance(first_message, (list, tuple)) and len(first_message) > 0:
            first_query = first_message[0]
        # Case 2: List[Dict] (OpenAI format or others)
        elif isinstance(first_message, dict):
            if first_message.get("role") == "user":
                first_query = first_message.get("content")
            elif "text" in first_message: # Fallback for other observed formats
                first_query = first_message["text"]

    if first_query and isinstance(first_query, str):
        # Split by common Chinese and English punctuation and whitespace
        delimiters = r"[,。?!,?!.\s]+"
        segments = re.split(delimiters, first_query)
        
        title = ""
        for seg in segments:
            if seg:
                title += seg
                if len(title) > 3:
                    return title[:50]  # Limit title length
        if title:
             return title[:50]

    # Rule 2: Use the system prompt
    if system_prompt:
        return system_prompt[:32]
        
    # Rule 3: Use the current time
    return datetime.now().strftime("%H:%M")


def get_history_df(history, lang: str):
    """
    Generates a language-aware DataFrame for the conversation history.
    """
    if not history:
        # Provide explicit column names for an empty DataFrame
        return pd.DataFrame({'ID': pd.Series(dtype='str'), get_text('chat_history_dataframe_header', lang): pd.Series(dtype='str')})
    
    df = pd.DataFrame(history)
    
    # Ensure columns exist before renaming
    if 'id' in df.columns and 'title' in df.columns:
        header_text = get_text('chat_history_dataframe_header', lang)
        # Ensure title is a string
        df['title'] = df['title'].astype(str)
        return df[['id', 'title']].rename(columns={'id': 'ID', 'title': header_text})
    else:
        return pd.DataFrame({'ID': pd.Series(dtype='str'), get_text('chat_history_dataframe_header', lang): pd.Series(dtype='str')})


def create_chat_tab(initial_lang: str, current_lang_state: gr.State):
    model_handler = ModelHandler()

    # Browser-side storage for conversation history and current ID
    conversation_store = gr.BrowserState(default_value=[], storage_key="ling_conversation_history")
    current_conversation_id = gr.BrowserState(default_value=None, storage_key="ling_current_conversation_id")

    def handle_new_chat(history, current_conv_id, lang):
        current_convo = next((c for c in history if c["id"] == current_conv_id), None) if history else None

        if current_convo and not current_convo.get("messages", []):
            return current_conv_id, history, [], gr.update(value=get_history_df(history, lang))

        conv_id = str(uuid.uuid4())
        new_convo_title = get_text('chat_new_conversation_title', lang)
        new_convo = {
            "id": conv_id, "title": new_convo_title,
            "messages": [], "timestamp": datetime.now().isoformat(),
            "system_prompt": "",
            "model": CHAT_MODEL_SPECS[LING_1T]["display_name"],
            "temperature": 0.7
        }
        updated_history = [new_convo] + (history or [])
        return conv_id, updated_history, [], gr.update(value=get_history_df(updated_history, lang))

    def load_conversation_from_df(df: pd.DataFrame, evt: gr.SelectData, history, lang):
        if evt.index is None or len(df) == 0:
            return None, [], "", CHAT_MODEL_SPECS[LING_1T]["display_name"], 0.7, ""

        selected_id = df.iloc[evt.index[0]]['ID']
        convo = next((c for c in history if c["id"] == selected_id), None)

        if convo:
            # Use .get() to provide defaults for old conversations
            system_prompt = convo.get("system_prompt", "")
            model = convo.get("model", CHAT_MODEL_SPECS[LING_1T]["display_name"])
            temperature = convo.get("temperature", 0.7)
            
            # Return updates for all components
            return selected_id, convo["messages"], system_prompt, model, temperature, ""
        
        # Fallback to creating a new chat if something goes wrong
        new_id, _, new_msgs, _ = handle_new_chat(history, None, lang)
        return new_id, new_msgs, "", CHAT_MODEL_SPECS[LING_1T]["display_name"], 0.7, ""

    with gr.Row(equal_height=False, elem_id="indicator-chat-tab"):
        with gr.Column(scale=1):
            new_chat_btn = gr.Button(get_text('chat_new_chat_button', initial_lang))
            history_df = gr.DataFrame(
                value=get_history_df(conversation_store.value, initial_lang),
                headers=["ID", get_text('chat_history_dataframe_header', initial_lang)],
                datatype=["str", "str"],
                interactive=False,
                visible=True,
                column_widths=["0%", "100%"]
            )

        with gr.Column(scale=4):
            chatbot = gr.Chatbot(height=500, placeholder=get_text('chat_chatbot_placeholder', initial_lang))
            with gr.Row():
                textbox = gr.Textbox(placeholder=get_text('chat_textbox_placeholder', initial_lang), container=False, scale=7)
                submit_btn = gr.Button(get_text('chat_submit_button', initial_lang), scale=1)
            
            recommended_title = gr.Markdown(get_text('chat_recommended_dialogues_title', initial_lang))
            recommended_dataset = gr.Dataset(
                components=[gr.Textbox(visible=False)],
                samples=[[item["task"]] for item in get_recommended_inputs(initial_lang)],
                label=get_text('chat_recommended_dataset_label', initial_lang),
                headers=[get_text('chat_recommended_dataset_header', initial_lang)],
            )

        with gr.Column(scale=1):
            model_dropdown, model_description_markdown = create_model_selector(
                model_specs=CHAT_MODEL_SPECS,
                default_model_constant=LING_1T,
                lang_state=current_lang_state,
                initial_lang=initial_lang
            )

            system_prompt_textbox = gr.Textbox(label=get_text('chat_system_prompt_label', initial_lang), lines=5, placeholder=get_text('chat_system_prompt_placeholder', initial_lang))
            temperature_slider = gr.Slider(minimum=0, maximum=1.0, value=0.7, step=0.1, label=get_text('chat_temperature_slider_label', initial_lang))

        # --- Event Handlers --- #
        def on_select_recommendation(evt: gr.SelectData, history, current_conv_id, lang):
            selected_task = evt.value[0]
            item = next((i for i in get_recommended_inputs(lang) if i["task"] == selected_task), None)
            if not item:
                return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()

            new_id, new_history, new_messages, history_df_update = handle_new_chat(history, current_conv_id, lang)

            return (
                new_id, new_history,
                gr.update(value=item["model"]),
                gr.update(value=item["system_prompt"]),
                gr.update(value=item["temperature"]),
                gr.update(value=item["user_message"]),
                history_df_update,
                new_messages
            )

        def chat_stream(conv_id, history, model_display_name, message, chat_history, system_prompt, temperature):
            if not message:
                yield chat_history
                return
            model_constant = next((k for k, v in CHAT_MODEL_SPECS.items() if v["display_name"] == model_display_name), LING_1T)
            response_generator = model_handler.get_response(model_constant, message, chat_history, system_prompt, temperature)
            for history_update in response_generator:
                yield history_update

        def on_chat_stream_complete(conv_id, history, final_chat_history, system_prompt, model_display_name, temperature, lang):
            current_convo = next((c for c in history if c["id"] == conv_id), None)
            if not current_convo:
                return history, gr.update()
            
            # Check if this is the first turn of a new conversation
            new_convo_title_default = get_text('chat_new_conversation_title', lang)
            is_new_conversation = current_convo["title"] == new_convo_title_default
            
            # If it's a new conversation and we have messages, generate a title and save metadata
            if is_new_conversation and len(final_chat_history) > len(current_convo.get("messages", [])):
                current_convo["system_prompt"] = system_prompt
                current_convo["model"] = model_display_name
                current_convo["temperature"] = temperature
                new_title = generate_conversation_title(final_chat_history, system_prompt)
                current_convo["title"] = new_title

            current_convo["messages"] = final_chat_history
            current_convo["timestamp"] = datetime.now().isoformat()

            history = sorted([c for c in history if c["id"] != conv_id] + [current_convo], key=lambda x: x["timestamp"], reverse=True)
            return history, gr.update(value=get_history_df(history, lang))
        
        # Store all components that need i18n updates
        components = {
            "new_chat_btn": new_chat_btn,
            "history_df": history_df,
            "chatbot": chatbot,
            "textbox": textbox,
            "submit_btn": submit_btn,
            "recommended_title": recommended_title,
            "recommended_dataset": recommended_dataset,
            "system_prompt_textbox": system_prompt_textbox,
            "temperature_slider": temperature_slider,
            "model_dropdown": model_dropdown,
            "model_description_markdown": model_description_markdown,
            # Non-updatable components needed for event handlers and app.py
            "conversation_store": conversation_store,
            "current_conversation_id": current_conversation_id,
        }

        # Wire event handlers
        recommended_dataset.select(on_select_recommendation, inputs=[conversation_store, current_conversation_id, current_lang_state], outputs=[current_conversation_id, conversation_store, model_dropdown, system_prompt_textbox, temperature_slider, textbox, history_df, chatbot], show_progress="hidden")

        submit_btn.click(
            chat_stream,
            [current_conversation_id, conversation_store, model_dropdown, textbox, chatbot, system_prompt_textbox, temperature_slider],
            [chatbot]
        ).then(
            on_chat_stream_complete,
            [current_conversation_id, conversation_store, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, current_lang_state],
            [conversation_store, history_df]
        )
        textbox.submit(
            chat_stream,
            [current_conversation_id, conversation_store, model_dropdown, textbox, chatbot, system_prompt_textbox, temperature_slider],
            [chatbot]
        ).then(
            on_chat_stream_complete,
            [current_conversation_id, conversation_store, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, current_lang_state],
            [conversation_store, history_df]
        )

        new_chat_btn.click(handle_new_chat, inputs=[conversation_store, current_conversation_id, current_lang_state], outputs=[current_conversation_id, conversation_store, chatbot, history_df])
        history_df.select(load_conversation_from_df, inputs=[history_df, conversation_store, current_lang_state], outputs=[current_conversation_id, chatbot, system_prompt_textbox, model_dropdown, temperature_slider, textbox])

    return components

def update_language(lang: str, components: dict):
    """
    Returns a dictionary mapping components to their gr.update calls for language change.
    """
    updates = {
        components["new_chat_btn"]: gr.update(value=get_text('chat_new_chat_button', lang)),
        components["history_df"]: gr.update(headers=["ID", get_text('chat_history_dataframe_header', lang)]),
        components["chatbot"]: gr.update(placeholder=get_text('chat_chatbot_placeholder', lang)),
        components["textbox"]: gr.update(placeholder=get_text('chat_textbox_placeholder', lang)),
        components["submit_btn"]: gr.update(value=get_text('chat_submit_button', lang)),
        components["recommended_title"]: gr.update(value=get_text('chat_recommended_dialogues_title', lang)),
        components["recommended_dataset"]: gr.update(
            samples=[[item["task"]] for item in get_recommended_inputs(lang)],
            label=get_text('chat_recommended_dataset_label', lang),
            headers=[get_text('chat_recommended_dataset_header', lang)],
        ),
        components["system_prompt_textbox"]: gr.update(label=get_text('chat_system_prompt_label', lang), placeholder=get_text('chat_system_prompt_placeholder', lang)),
        components["temperature_slider"]: gr.update(label=get_text('chat_temperature_slider_label', lang)),
    }
    return updates