File size: 13,136 Bytes
38ebcd8
 
 
 
 
8a7699c
 
84a332c
bb70ab8
dfa50d5
9ebd152
73dea88
38ebcd8
 
 
 
 
 
dfa50d5
6c4626c
38ebcd8
6c4626c
38ebcd8
73dea88
 
 
 
 
 
 
 
 
 
 
 
 
38ebcd8
63d7752
38ebcd8
9ebd152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84a332c
 
bd56ab7
38ebcd8
 
16bdc7c
1c242d9
e324c0f
19d4c11
 
 
dfa50d5
38ebcd8
63d7752
 
 
73dea88
38ebcd8
 
73dea88
 
 
 
 
dfa50d5
73dea88
63d7752
38ebcd8
 
63d7752
 
 
73dea88
63d7752
 
 
 
73dea88
38ebcd8
63d7752
73dea88
 
 
 
16bdc7c
38ebcd8
73dea88
2a62501
 
 
38ebcd8
8a7699c
 
62d447f
 
 
 
 
 
 
 
 
 
 
 
8a7699c
62d447f
8a7699c
62d447f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8a7699c
 
de3f546
63d7752
38ebcd8
63d7752
38ebcd8
63d7752
 
de3f546
63d7752
de3f546
63d7752
de3f546
63d7752
de3f546
63d7752
de3f546
63d7752
38ebcd8
de3f546
aabffe4
73dea88
de3f546
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73dea88
 
 
e4d7d00
73dea88
38ebcd8
 
de3f546
38ebcd8
7048c23
 
 
71b16cf
 
7048c23
074add4
7048c23
 
 
9fdc42d
56a0087
84a332c
9fdc42d
 
 
56a0087
074add4
491e0be
62d447f
 
84a332c
 
 
 
 
d10ba8f
84a332c
16bdc7c
84a332c
db02b31
 
 
84a332c
 
 
8a7699c
 
16bdc7c
491e0be
 
074add4
 
 
 
8a7699c
16bdc7c
 
 
19d4c11
074add4
 
 
8a7699c
 
 
 
 
 
 
 
 
 
 
 
7048c23
 
 
 
38ebcd8
7048c23
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
# main.py
import gradio as gr
from analyzer import analyze_teacher_dashboard
from cluster_insight import cluster_and_visualize
from qwen_api import call_qwen
import pandas as pd
import re
import tempfile  
import os
# ================== LLM 教学建议 ==================
def generate_teaching_advice1(sankey_fig, cluster_stats):
    prompt = """
你是一名GIS实验教学专家,基于以下分析结果,生成教学优化方案:
【桑基图分析】
- 学生反馈从 s1→s4 的主要流向:核密度 → 参数设置 → 应用场景
- 最粗路径:核密度分析 → 搜索半径选择 → 城市规划应用
【聚类分析】
"""
    for s in cluster_stats[:3]:
        prompt += f"- 聚类 {s['cluster_id']}{s['keyword']}{s['size']}条,占{s['ratio']:.1%})\n"
        prompt += f"  代表句:{s['rep_sentence'][:100]}\n"

    prompt += """
【要求】
1. 诊断核心教学痛点(3条)
2. 提出针对性优化措施(微课/演示/作业)
3. 设计 1 个 2 分钟微课脚本(标题+3步演示)
4. 建议 1 个课后作业(验证学生掌握)

【输出格式】
# 教学优化方案
## 1. 核心痛点
## 2. 优化措施
## 3. 微课脚本
## 4. 课后作业
"""
    advice = call_qwen(prompt)
    return f"<pre style='background:#f8f9fa; padding:20px; border-radius:12px; white-space: pre-wrap; font-family: Microsoft YaHei; line-height:1.6;'>{advice}</pre>"

# ================== LLM 教学建议---测试版 ==================
def generate_teaching_advice(sankey_fig, cluster_stats):
    # 临时替换LLM调用(30秒内必完成)
    advice = """
    # 教学优化方案
    ## 1. 核心痛点
    1. 核密度分析的搜索半径参数设置缺乏实操指导
    2. 空间连接功能的应用场景与实操步骤脱节
    3. 栅格计算器的公式编写逻辑讲解不清晰
    
    ## 2. 优化措施
    1. 制作参数设置微课,结合案例演示不同场景下的取值标准
    2. 增加空间连接功能的分步实操视频,配套场景化习题
    3. 提供栅格计算器常用公式模板,附详细注释
    
    ## 3. 微课脚本
    ### 标题:3分钟掌握核密度分析搜索半径设置
    1. (0-30秒)明确搜索半径的核心作用:影响密度场平滑度
    2. (30-90秒)演示城市POI数据的半径设置(500米):工具位置→参数面板→取值依据
    3. (90-180秒)对比不同半径效果(300米/500米/1000米),总结取值规律
    
    ## 4. 课后作业
    基于提供的城市餐饮POI数据,分别设置300米、500米、1000米搜索半径,生成3张核密度图,分析不同半径对结果的影响并提交报告
    """
    return f"<pre style='background:#f8f9fa; padding:20px; border-radius:12px; white-space: pre-wrap; font-family: Microsoft YaHei; line-height:1.6;'>{advice}</pre>"

    # ================== 主分析函数 ==================
def analyze_report(excel_path):
    if not excel_path:
        return "请上传 Excel 文件", None, None, None, "", ""

    try:
        sankey_fig, sb64 = analyze_teacher_dashboard(excel_path=excel_path)
        cluster_fig, cb64, cluster_stats  = cluster_and_visualize(excel_path=excel_path)
        advice = generate_teaching_advice(sankey_fig, cluster_stats)
        print(f"{sankey_fig}{sb64}生成成功!")
        print(f"{cluster_fig}{cb64}生成成功!")
        
        # 统计表格
        stats_table = """
            <h3 style="color:#1976d2; text-align:center; margin:40px 0 15px;">聚类主题统计</h3>
            <table border="1" style="width:100%; max-width:900px; margin:0 auto; border-collapse: collapse; text-align:center; font-size:14px;">
                <tr style="background:#f0f0f0;"><th>聚类</th><th>主题关键词</th><th>反馈数</th><th>占比</th><th>代表句</th></tr>
        """
        for s in cluster_stats:
            stats_table += f"""
                <tr>
                    <td>{s['cluster_id']}</td>
                    <td><strong>{s['keyword']}</strong></td>
                    <td>{s['size']}</td>
                    <td>{s['ratio']:.1%}</td>
                    <td style="text-align:left; max-width:400px;">{s['rep_sentence'][:80]}...</td>
                </tr>
            """.format(s=s)
        stats_table += "</table>"

        # 最终报告(只放文字部分)
        html_report = f"""
            <div style="font-family:'Microsoft YaHei',sans-serif; max-width:1000px; margin:40px auto; padding:20px;">
                <h1 style="text-align:center; color:#1e88e5;">EGISInsight</h1>
                <p style="text-align:center; color:#555; font-size:17px;">GIS 教学智能体 · 循证教学优化</p>
                <hr style="border:1px solid #eee; margin:30px 0;">
                {stats_table}
                <div style="padding:25px; background:#f8f9fa; border-radius:12px; margin-top:30px;">
                    {advice}
                </div>
                <p style="text-align:center; color:#999; margin-top:50px; font-size:13px;">
                    EGISInsight © 2025 | 从数据到教学内容改革
                </p>
            </div>
        """
        return html_report, sankey_fig, cluster_fig, cluster_stats, sb64, cb64

    except Exception as e:
        # 问题1修复:异常逻辑也返回6个值(补全空字符串)
        error_html = f"<p style='color:red; text-align:center;'>分析失败:{str(e)}</p>"
        return error_html, None, None, None, "", ""


# 核心工具函数:把平台传递的纯文本 → DataFrame(不变)
def parse_text_to_df(raw_text: str):
    """
    步骤:
    1. 按制表符\t分割所有单元格(你的原有逻辑)
    2. 过滤空单元格,得到纯数据列表
    3. 按每行5列重新分组,不足5列的补None
    4. 手动添加表头,生成DataFrame
    """
    # 1. 清理原始文本(避免多余空字符干扰)
    cleaned_text = re.sub(r'\n+', '', raw_text.strip())  # 去掉所有换行(因为你按\t分割,换行没用)
    cleaned_text = re.sub(r'\t+', '\t', cleaned_text)    # 合并连续制表符
    cleaned_text = re.sub(r'^\t|\t$', '', cleaned_text)  # 去掉首尾制表符

    # 2. 按制表符分割,过滤空单元格(你的原有逻辑,稍作优化)
    lines = []
    for cell in cleaned_text.split('\t'):
        cell_stripped = cell.strip()
        # 跳过空单元格(仅保留有内容的)
        if cell_stripped or cell_stripped == '0':  # 避免把"0"当成空(如果有编号为0的情况)
            lines.append(cell_stripped)
        else:
            lines.append(None)  # 空单元格用None填充,方便后续分组

    # 关键修改:去掉第一个元素(sheet名,如ex01)
    if len(lines) > 0:
        lines = lines[1:]  # 切片:保留从索引1开始的所有元素,去掉索引0(第一个元素)
        print(f"已去掉sheet名,剩余元素数量:{len(lines)}")
    else:
        raise Exception("分割后无有效数据,无法解析")

    # 关键修改2:再去掉第一个元素(原始表头 no/s1/s2/s3/s4)
    if len(lines) > 5 and lines[0] == 'no':  # 确认第一个元素是表头,再删除
        lines = lines[5:]
    else:
        # 若没有原始表头(极端情况),直接继续(后续用我们的固定表头)
        pass
    # 3. 核心:按每行5列重新分组(关键逻辑)
    header = ['no', 's1', 's2', 's3', 's4']  # 固定表头
    data_rows = []  # 存储分组后的每行数据

    # 计算需要分多少组(向上取整,避免遗漏最后几个元素)
    total_cells = len(lines[1:])
    total_rows = (total_cells + 4) // 5  # 向上取整(比如11个元素→3行:5+5+1)

    # 按5个元素一组拆分,不足5列的补None
    for i in range(total_rows):
        # 截取当前行的5个元素(左闭右开区间)
        start_idx = i * 5
        end_idx = start_idx + 5
        row = lines[start_idx:end_idx]
        # 不足5列的补None(确保每行都是5列)
        while len(row) < 5:
            row.append(None)
        data_rows.append(row)

    # 4. 生成DataFrame,清理无效数据
    df = pd.DataFrame(data_rows, columns=header)
    # 清理:去掉no列为空的行、s1-s4全为空的行
    df = df.dropna(subset=['no'], how='any')
    df = df.dropna(subset=['s1', 's2', 's3', 's4'], how='all')

    return df

# ================== 完美兼容版界面(老版本也能居中 + 控制高度)==================
with gr.Blocks(theme=gr.themes.Soft(), title="EGISInsight") as demo:
    gr.Markdown("# GIS实验报告智能分析系统")
    gr.Markdown("**上传学生反馈 Excel → 1秒生成教学决策图 + AI教案**")

    file_input = gr.File(label="上传 ex02.xlsx(需含 s1-s4 列)", file_types=[".xlsx"])

    # 标题 + 桑基图(完美居中 + 固定高度)
    gr.HTML('<h2 style="text-align:center; color:#1976d2; margin:40px 0 10px;">1. 学生反馈流向分析(交互桑基图)</h2>')
    sankey_plot = gr.Plot(elem_id="sankey-plot")          # 去掉 height,加 elem_id

    # 标题 + 聚类图
    gr.HTML('<h2 style="text-align:center; color:#388e3c; margin:50px 0 10px;">2. 学生反馈主题聚类可视化</h2>')
    cluster_plot = gr.Plot(elem_id="cluster-plot")        # 去掉 height,加 elem_id

    # 文字报告
    html_report = gr.HTML()

    # 隐藏统计
    stats_json = gr.JSON(visible=False)

    # 关键:加一段 CSS 强制高度和居中
    demo.load(
        None,
        None,
        None,
        js="""
        () => {
            const style = document.createElement('style');
            style.innerHTML = `
                #sankey-plot, #cluster-plot {
                    height: 560px !important;
                    width: 100% !important;
                    max-width: 1100px !important;
                    margin: 0 auto !important;
                    display: block !important;
                }
                #sankey-plot > div, #cluster-plot > div {
                    height: 100% !important;
                }
            `;
            document.head.appendChild(style);
        }
        """
    )

    file_input.change(
        fn=analyze_report,
        inputs=file_input,
        outputs=[html_report, sankey_plot, cluster_plot, stats_json]
    )

    gr.Markdown("---")
    gr.Markdown("<p style='text-align:center; color:#666;'>通义千问大模型实时生成教学优化方案</p>")

# if __name__ == "__main__":
#    demo.launch(server_name="0.0.0.0", share=True)

# ================== 大赛平台专用 API(最终修正版)==================
from fastapi import FastAPI, File, UploadFile, Form
import uvicorn
from io import BytesIO

app = FastAPI()

@app.post("/api/plugin")
async def plugin_api(
    file: str = Form(...),
    token: str = Form(...),
    timestamp: str = Form(...),
    signature: str = Form(...)
):
    try:
        # print(type(file), file)
        df = parse_text_to_df(file)
        print(f"解析出 {len(df)} 条有效反馈")

        # 关键:创建本地临时Excel文件(自动生成真实路径,用完删除)
        with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as temp_file:
            df.to_excel(temp_file, index=False)  # 写入临时文件
            temp_file_path = temp_file.name  # 拿到临时文件的真实路径(比如:C:\Users\XXX\AppData\Local\Temp\tmpxxxx.xlsx)
        print(df)
        # 调用分析函数(传临时文件的真实路径)
        html_report, sankey_fig, cluster_fig, stats, sb64, cb64 = analyze_report(temp_file_path)

        print(f"{sankey_fig}生成成功!")
        print(f"{cluster_fig}生成成功!")
        
        # 用完删除临时文件(避免占用空间)
        os.remove(temp_file_path)
        print(f"临时文件已删除:{temp_file_path}")

        # 按平台要求返回结果
        # 按平台要求返回结果(用 sb64/cb64 替换 to_json())
        print(f"{sb64}生成成功!")
        print(f"{cb64}生成成功!")
        return {
            "code": 200,
            "message": "success",
            "data": {
                "html_report": html_report if html_report else "",
                # 直接用已有的base64字符串,不用再转json!
                "sankey_image": sb64 if sb64 else "",  # 键名改成sankey_image(符合平台图片命名规范)
                "cluster_image": cb64 if cb64 else "",  # 键名改成cluster_image
                "statistics": stats if stats else []  # 格式化后的统计数据
            }
        }
    except Exception as e:
        error_msg = f"分析失败:{str(e)}"
        print(error_msg)
        return {
            "code": 500,
            "message": error_msg,
            "data": {
                "html_report": "",
                "sankey_fig": "",
                "cluster_fig": "",
                "stats": []
            }
        }

# 保留原界面
app = gr.mount_gradio_app(app, demo, path="/")

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)