mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 11:45:24 +03:00
add ability to update description in frontend
This commit is contained in:
parent
99cdf10e1c
commit
f220e4d3f3
@ -174,6 +174,8 @@ Events from the database. Accepts the following query string parameters:
|
||||
| `is_submitted` | int | Filter events that are submitted to Frigate+ (0 or 1) |
|
||||
| `min_length` | float | Minimum length of the event |
|
||||
| `max_length` | float | Maximum length of the event |
|
||||
| `search` | str | Search query for semantic search |
|
||||
| `like` | str | Event ID for thumbnail similarity search |
|
||||
|
||||
### `GET /api/timeline`
|
||||
|
||||
@ -225,7 +227,17 @@ Sub labels must be 100 characters or shorter.
|
||||
```json
|
||||
{
|
||||
"subLabel": "some_string",
|
||||
"subLabelScore": 0.79,
|
||||
"subLabelScore": 0.79
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/events/<id>/description`
|
||||
|
||||
Set a description for an event to add details about what is occurring. Used during semantic search.
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "This is a black and white dog walking on the sidewalk."
|
||||
}
|
||||
```
|
||||
|
||||
@ -298,7 +310,7 @@ It is also possible to export this recording as a timelapse.
|
||||
|
||||
```json
|
||||
{
|
||||
"playback": "realtime", // playback factor: realtime or timelapse_25x
|
||||
"playback": "realtime" // playback factor: realtime or timelapse_25x
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ from frigate.const import (
|
||||
MAX_SEGMENT_DURATION,
|
||||
RECORD_DIR,
|
||||
)
|
||||
from frigate.embeddings import Embeddings
|
||||
from frigate.embeddings import Embeddings, get_metadata
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.models import Event, Previews, Recordings, Regions, Timeline
|
||||
from frigate.object_processing import TrackedObject
|
||||
@ -449,6 +449,51 @@ def set_sub_label(id):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/events/<id>/description", methods=("POST",))
|
||||
def set_description(id):
|
||||
try:
|
||||
event: Event = Event.get(Event.id == id)
|
||||
except DoesNotExist:
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Event " + id + " not found"}), 404
|
||||
)
|
||||
|
||||
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||
new_description = json.get("description")
|
||||
|
||||
if new_description is None or len(new_description) == 0:
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "description cannot be empty",
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
event.data["description"] = new_description
|
||||
event.save()
|
||||
|
||||
# If semantic search is enabled, update the index
|
||||
if current_app.embeddings is not None:
|
||||
current_app.embeddings.description.upsert(
|
||||
documents=[new_description],
|
||||
metadatas=[get_metadata(event)],
|
||||
ids=[id],
|
||||
)
|
||||
|
||||
return make_response(
|
||||
jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Event " + id + " description set to " + new_description,
|
||||
}
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/labels")
|
||||
def get_labels():
|
||||
camera = request.args.get("camera", type=str, default="")
|
||||
|
||||
66
web-old/src/components/TextArea.jsx
Normal file
66
web-old/src/components/TextArea.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { h } from 'preact';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
|
||||
export default function TextArea({
|
||||
helpText,
|
||||
keyboardType = 'text',
|
||||
inputRef,
|
||||
onBlur,
|
||||
onChangeText,
|
||||
onFocus,
|
||||
readonly,
|
||||
value: propValue = '',
|
||||
...props
|
||||
}) {
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const [value, setValue] = useState(propValue);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(event) => {
|
||||
setFocused(true);
|
||||
onFocus && onFocus(event);
|
||||
},
|
||||
[onFocus]
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(event) => {
|
||||
setFocused(false);
|
||||
onBlur && onBlur(event);
|
||||
},
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event) => {
|
||||
const { value } = event.target;
|
||||
setValue(value);
|
||||
onChangeText && onChangeText(value);
|
||||
},
|
||||
[onChangeText, setValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (propValue !== value) {
|
||||
setValue(propValue);
|
||||
}
|
||||
// DO NOT include `value`
|
||||
}, [propValue, setValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<textarea
|
||||
className="block p-2.5 w-full h-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
// className="h-6 mt-6 w-full bg-transparent border-0 focus:outline-none focus:ring-0"
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
onInput={handleChange}
|
||||
readOnly={readonly}
|
||||
tabIndex="0"
|
||||
{...props}
|
||||
>
|
||||
{value}
|
||||
</textarea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -28,6 +28,7 @@ import Dialog from '../components/Dialog';
|
||||
import MultiSelect from '../components/MultiSelect';
|
||||
import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil';
|
||||
import TextField from '../components/TextField';
|
||||
import TextArea from '../components/TextArea';
|
||||
import TimeAgo from '../components/TimeAgo';
|
||||
import Timepicker from '../components/TimePicker';
|
||||
import TimelineSummary from '../components/TimelineSummary';
|
||||
@ -259,6 +260,15 @@ export default function Events({ path, ...props }) {
|
||||
setState({ ...state, showDownloadMenu: true });
|
||||
};
|
||||
|
||||
let descriptionTimeout;
|
||||
const onUpdateDescription = async (event, description) => {
|
||||
clearTimeout(descriptionTimeout);
|
||||
descriptionTimeout = setTimeout(async () => {
|
||||
event.data.description = description;
|
||||
await axios.post(`events/${event.id}/description`, { description });
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const showSimilarEvents = (event_id, e) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
@ -727,6 +737,7 @@ export default function Events({ path, ...props }) {
|
||||
});
|
||||
}}
|
||||
onSave={onSave}
|
||||
onUpdateDescription={onUpdateDescription}
|
||||
showSimilarEvents={showSimilarEvents}
|
||||
showSubmitToPlus={showSubmitToPlus}
|
||||
/>
|
||||
@ -768,6 +779,7 @@ export default function Events({ path, ...props }) {
|
||||
});
|
||||
}}
|
||||
onSave={onSave}
|
||||
onUpdateDescription={onUpdateDescription}
|
||||
showSimilarEvents={showSimilarEvents}
|
||||
showSubmitToPlus={showSubmitToPlus}
|
||||
/>
|
||||
@ -801,6 +813,7 @@ function Event({
|
||||
onDownloadClick,
|
||||
onReady,
|
||||
onSave,
|
||||
onUpdateDescription,
|
||||
showSimilarEvents,
|
||||
showSubmitToPlus,
|
||||
}) {
|
||||
@ -837,7 +850,18 @@ function Event({
|
||||
{event.sub_label ? `: ${event.sub_label.replaceAll('_', ' ')}` : null}
|
||||
</div>
|
||||
{event?.data?.description ? (
|
||||
<div className="text-sm flex flex-col grow pb-2">{event.data.description}</div>
|
||||
<div className="flex flex-col pb-2">
|
||||
{viewEvent === event.id ? (
|
||||
<TextArea
|
||||
label=""
|
||||
value={event.data.description}
|
||||
onChangeText={(value) => onUpdateDescription(event, value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<div class="text-sm line-clamp-2">{event.data.description}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-sm flex">
|
||||
<Clock className="h-5 w-5 mr-2 inline" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user