Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Add: Custom HTTP tracking endpoint via `ZNAI_TRACK_ACTIVITY_URL` environemnt variable
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Znai Slack Bot - Local Testing Instructions
# Znai Enterprise Sample Server - Local Testing Instructions

## Prerequisites

Expand All @@ -23,7 +23,7 @@
### 2. Install Dependencies

```bash
cd znai-slack-bot
cd znai-enterprise-sample-server
pip install -r requirements.txt
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "znai-slack-bot"
name = "znai-enterprise-sample-server"
version = "1.0.0"
description = "Slack bot for Znai documentation questions"
description = "Enterprise sample server for Znai documentation questions"
requires-python = ">=3.9"
dependencies = [
"flask==3.0.0",
Expand All @@ -14,7 +14,7 @@ dependencies = [
]

[project.scripts]
znai-slack-bot = "slack_bot:main"
znai-enterprise-sample-server = "slack_bot:main"

[tool.black]
line-length = 88
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import csv
import uuid
from datetime import datetime
from flask import Flask, request, jsonify
from flask_cors import CORS
from slack_sdk import WebClient
Expand All @@ -28,6 +29,9 @@
CSV_FILE = "active_questions.csv"
CSV_HEADERS = ["id", "timestamp", "pageId", "pageUrl", "context", "selectedText", "selectedPrefix", "selectedSuffix", "question", "slackLink", "username", "channel", "slackMessageTs", "resolved"]

TRACKING_CSV_FILE = "tracking_events.csv"
TRACKING_CSV_HEADERS = ["docId", "pageId", "timestamp", "eventType", "data"]

@app.route('/ask-in-slack', methods=['POST'])
def ask_in_slack():
try:
Expand Down Expand Up @@ -183,29 +187,79 @@ def get_active_questions():
def resolve_slack_question(ts):
try:
print(f"=== resolve-slack-question request received for ts: {ts} ===")

updated = update_question_completion_status(ts, True)

if updated:
print(f"Successfully marked question with ts {ts} as resolved")
return jsonify({"success": True, "message": f"Question {ts} marked as resolved"}), 200
else:
print(f"Question with ts {ts} not found")
return jsonify({"error": f"Question with ts {ts} not found"}), 404

except Exception as e:
print(f"Error resolving question: {type(e).__name__}: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500

@app.route('/track-activity', methods=['POST'])
def track_event():
try:
data = request.get_json()

if not data:
return jsonify({"error": "No JSON data provided"}), 400

doc_id = data.get('docId')
event_type = data.get('eventType')
page_id = data.get('pageId')
event_data = data.get('data', {})

if not event_type or not page_id:
return jsonify({"error": "Missing required fields: eventType and pageId"}), 400

timestamp = datetime.utcnow().isoformat()

persist_tracking_event({
'docId': doc_id,
'pageId': page_id,
'timestamp': timestamp,
'eventType': event_type,
'data': json.dumps(event_data) if event_data else ''
})

return jsonify({"success": True}), 200

except Exception as e:
print(f"Error tracking event: {type(e).__name__}: {str(e)}")
import traceback
traceback.print_exc()
return jsonify({"error": str(e)}), 500

def persist_tracking_event(event):
file_exists = os.path.exists(TRACKING_CSV_FILE)

with open(TRACKING_CSV_FILE, 'a', newline='', encoding='utf-8') as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=TRACKING_CSV_HEADERS)

if not file_exists:
writer.writeheader()

writer.writerow(event)

def format_slack_message(username, question, context, page_url):
message_parts = [f"@{username} <{page_url}|asked>: {question}"]

if context:
message_parts.append(context)

return "\n\n".join(message_parts)

# Erase tracking CSV file on server start
if os.path.exists(TRACKING_CSV_FILE):
os.remove(TRACKING_CSV_FILE)
print(f"Erased existing tracking file: {TRACKING_CSV_FILE}")

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5111, debug=True)
6 changes: 5 additions & 1 deletion znai-reactjs/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import { echartDemo } from "./doc-elements/charts/Echart.demo";
import { chartsPresentationDemo } from "./doc-elements/charts/EchartPresentation.demo";
import { smartBulletListsDemo } from "./doc-elements/bullets/SmarlBulletList.demo";
import { tooltipDemo } from "./components/Tooltip.demo";
import { dismissableErrorIndicatorsDemo } from "./components/DismissableErrorIndicators.demo";
import { annotatedImageWithOrderedListDemo } from "./doc-elements/images/AnnotatedImageWithOrderedList.demo";
import { pythonMethodDemo } from "./doc-elements/python/PythonMethod.demo";
import { openApiAndMethodAndUrlDemo } from "./doc-elements/open-api/OpenApiAndMethodAndUrl.demo";
Expand Down Expand Up @@ -134,7 +135,10 @@ updateGlobalDocReferences({

const registries = new Registries();

registries.add("components").registerAsRows("tooltip", tooltipDemo);
registries
.add("components")
.registerAsRows("tooltip", tooltipDemo)
.registerAsRows("dismissable error indicators", dismissableErrorIndicatorsDemo);

registries
.add("text")
Expand Down
63 changes: 63 additions & 0 deletions znai-reactjs/src/components/DismissableErrorIndicators.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2025 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.znai-dismissable-error-indicators {
position: fixed;
top: 16px;
right: 16px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 400px;
}

.znai-error-indicator-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: rgba(255, 152, 0, 0.15);
border: 1px solid rgba(255, 152, 0, 0.5);
border-radius: 6px;
padding: 10px 14px;
font-size: 14px;
line-height: 1.4;
}

.znai-error-indicator-close {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 0;
flex-shrink: 0;
display: flex;
align-items: center;
transition: color 0.2s ease;
}

.znai-error-indicator-close:hover {
color: #333;
}

.theme-znai-dark .znai-error-indicator-close {
color: #aaa;
}

.theme-znai-dark .znai-error-indicator-close:hover {
color: #ddd;
}
118 changes: 118 additions & 0 deletions znai-reactjs/src/components/DismissableErrorIndicators.demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2025 znai maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React from "react";

import { Registry } from "react-component-viewer";
import { DismissableErrorIndicators, errorNotifications } from "./DismissableErrorIndicators";

export function dismissableErrorIndicatorsDemo(registry: Registry) {
registry.add("single error", () => {
return (
<div>
<button
onClick={() =>
errorNotifications.notifyError({
id: "demo-error-1",
message: "Connection to server failed",
})
}
>
Notify Error
</button>
<DismissableErrorIndicators />
</div>
);
});

registry.add("multiple errors", () => {
return (
<div>
<button
onClick={() =>
errorNotifications.notifyError({
id: "demo-error-slack",
message: "Slack conversations are offline",
})
}
>
Notify Slack Error
</button>
<button
onClick={() =>
errorNotifications.notifyError({
id: "demo-error-tracking",
message: "Activity tracking is offline",
})
}
>
Notify Tracking Error
</button>
<button
onClick={() =>
errorNotifications.notifyError({
id: "demo-error-database",
message: "Database connection lost",
})
}
>
Notify Database Error
</button>
<DismissableErrorIndicators />
</div>
);
});

registry.add("persistent dismissal", () => {
let attempt = 1;

return (
<div>
<p>Dismiss the error, then click the button again. The error will not reappear because it uses the same ID.</p>
<button
onClick={() => {
errorNotifications.notifyError({
id: "demo-persistent-error",
message: `Connection failed (attempt ${attempt++})`,
});
}}
>
Retry Connection
</button>
<DismissableErrorIndicators />
</div>
);
});

registry.add("long message", () => {
return (
<div>
<button
onClick={() =>
errorNotifications.notifyError({
id: "demo-long-error",
message:
"Failed to connect to the remote server. Please check your network connection and try again. If the problem persists, contact support.",
})
}
>
Notify Long Error
</button>
<DismissableErrorIndicators />
</div>
);
});
}
Loading